一、前言

本文作者介绍了什么是 E2E 测试以及 E2E 测试测什么,并从对于被测系统、测试用例、测试自动化工具、测试者四个方面的要求,介绍了如何保证 E2E 测试有效性,干货满满,值得学习。

二、什么是 E2E 测试

相信每一个对自动化测试感兴趣,并且付诸实践去了解测试理论的人可能都听说过 “测试金字塔 (Test Pyramid)”。 测试金字塔的核心理念是分层测试,即按照不同的隔离粒度来测试。例如我们可以将一个项目看作是一个个方法/函数组成的,那么可以对每个方法/函数进行测试,这大概是目前测试的最小隔离粒度了,通常称之为单元测试。我们也可以将一个项目看作是一个个模块/服务组成的,对每个服务进行测试,这里的隔离颗粒度就稍大。然后各个服务之间串联起来,最终只有一个用户界面暴露给用户,从这个用户界面上发出的服务请求,到用户界面收到响应做出变化,数据流经的整个链路我们称为端到端,以这样的场景为测试颗粒度成为端到端测试。

测试金字塔定义了分层测试理念,但是具体每一层是什么却没有定论,下图是 Mike Cohn 最初提出测试金字塔这一概念时的样子,但是实践中大家的分层方法各有不同,但是不变的是:隔离颗粒度越小,在这一层的测试用例就应该越多,每个测试用例自身也应该越小,运行速度也越快。

传统的端到端测试往往是从用户界面出发,测试整个技术栈,但是随着客户端种类越来越丰富,前端框架越来越成熟,UI 可以单独写单元测试了,而可以把 “从发出的后端 API 请求,到接收到响应” 这一段作为端对端的测试主题。

三、E2E 测试测什么

E2E 测试旨在复制真实的用户场景,以便可以验证系统的集成和数据完整性。从本质上讲,测试会遍历应用程序可以执行的每个操作,以测试应用程序如何与硬件、网络连接、外部依赖项、数据库和其他应用程序进行通信。

当你写 E2E 用例时,想象你就是用户,用户在使用某个功能时,第一步要做什么,第二步要做什么,每一步期望会得到什么样的结果。E2E 的宗旨就是保证用户使用我们的系统可以得到他想要的功能。

不要在 E2E 测试中测试异常流,比如下游服务失败,数据库超时等等情况。这些异常流应该在更小颗粒度的测试中去做,比如单元测试或者接口测试,事实上在小颗粒度的测试中也更容易做这些测试。

四、如何保证 E2E 测试有效

1.对被测系统的要求

不是任何代码都可以被自动化测试的。这一点对单元测试和大型测试都是适用的。对于单元测试,可测的代码需要抽象良好,解耦合理,这一点是很好理解的。对于大型测试,被测系统的要求更加复杂,一个最核心的要求是被测服务要具备可观测性 (Observability).(软件的可测试性要求有许多提法,但是所有的提法里一定有可观测性这一指标)。系统的可观测性是指系统通过其接口能暴露系统的内部行为或状态.

为了表述的方便我们先举一个例子。一个优惠券业务系统中,某一类优惠券只发放给高级会员,同时要求买满 1000 指定类目商品才能使用。现在考虑 “在订单中使用优惠券” 这一接口。这一接口的入参有用户信息,订单信息和优惠券的 ID。这个接口的调用流程如下:


其中 3.订单是否满 1000 是当前服务的内部逻辑,不是服务调用。1 和 2 均是对下游服务的调用。

  1. 任何一个接口返回的状态码应该是收敛的,或者说是可枚举的。上面例子中虽然下游服务并不复杂,但是系统的具体失败点可以有非常多,仅以会员服务的调用为例:a. 调用会员服务超时,b. 名字服务中找不到会员服务,c. 会员服务返回用户非高级会员,d. 会员服务返回用户未注册, e. 会员服务返回 token 过期。类似的失败点在商品服务中也会有,当前服务的下游依赖服务越多,具体的失败点也就会越多,直接下游服务数量会增加失败点的常数量级 (加法关系),而间接下游服务的数量会增加失败点的几何量级 (乘数关系)。我们不可能把下游暴露的错误原原本本地透传到客户端去。

  2. 接口应该按照用户可选择的行为来归类错误。以上面会员服务调用引起的失败为例,面对失败用户无非有 3 大类选择:

3.状态码可以收敛,但是失败点要记录下来。客户端不必关心失败点 (也即,端到端调用的返回信息可能不足以定位失败点),但是开发者排查错误时是需要找到具体失败点的。记录失败点的手段有多种,可以使用日志系统记录下来,可以在相同的错误码中使用不同的错误信息,也可以在全链路追踪中埋点。

状态码和状态消息是面向客户的,拿着它们去找失败点可能会定位精度不足。全链路追踪是微服务建设可观测性的关键中间件,OpenTelemetry 定义了一套全链路追踪的协议和 SDK。接入后,每一次端到端调用都会有一个 Trace ID,通过 Trace ID 可以查询调用链的详细情况,链路中每一次调用都会有 RED (Request, Error, Duration) 信息, 同时我们也可以往链路中追加一些其他有用的信息,比如 session id 来记录链路的用户信息。

全链路追踪非常有价值,任何现代的后端系统都应该接入一套 OpenTelemetry 的实现,使用 OpenTelemetry 的好处是其协议具有通用性,可以很好地被各种工具支持。每一个严肃的业务开发都有必要了解这一块的知识,当你需要排查一个线上问题而无从下手时就会深有体会。

接入全链路追踪后,在接口测试和 E2E 测试时,应该使用统一的格式将 Trace ID 打印到 test log 中,一旦测试失败,就可以拿着 Trace ID 去快速定位失败点。

2.对测试用例的要求

测试用例的职责是对可能的风险点作问题排查。没有用例/用例集能完整地测试一个系统,然而我们追求的目标却是尽可能完全地去测试一个系统。这很难,因此我们要写高质量的用例,不要写无意义或者低价值的用例。

不论是何种颗粒度的测试用例,其在逻辑上的步骤都是一样的,虽然有不同提法,有的叫 AAA (Arrange, Act, Assert), 有的叫 Given-When-Then, 个人认为最准确的提法是下面四个步骤:Setup, Invoke, Assert, Teardown

  1. Setup 阶段是 flakiness 的常见来源。个人经验这是很多初涉自动化测试的同学最容易出问题的地方。在 Setup 阶段需要保证被测系统处在一个确定的状态,想象被测系统是一个状态机,我们的测试内容是在某个特定状态下,给系统一个输入,保证其进入到我们期望的状态,并/或给出我们想要的输出。当然那种纯 “无状态” 系统可以认为是只有一种状态的状态机。广义的 setup 包括测试 binary 执行前的流水线前置准备步骤,和单个用例中准备输入参数的代码调用。举个例子说明 setup 阶段我们要做什么:一个 “删除文件” 的接口,接口的参数有用户 token,用户要删的文件 id,被测的系统有多种状态,用户 token 是否过期,文件是否存在,文件存在的情况下该用户对该文件是否有删除的权限等等。每一个可能的状态的排列组合都应该有一个用例去测试。假设其中一个用例是期望已登录用户对没有权限的文件进行删除时,接口报错 “Permission Denied", 那么在 setup 阶段我们要做的事情有:

准备好了这些我们才能进入下一个步骤:调用接口 ( invoke)。上面的表述中 “一定” 和 “必须” 就是核心。下面列举几个实践中容易犯错的情景:
在 E2E 测试中,正确部署往往被误解为只是把被测服务部署到正确的环境里,其实这里还要求所有被测服务的下游服务也必须处于可预期的正确状态。我经常听到有人说 E2E 测试因为下游服务而导致的失败,然后就不管了,这是不对的,测试者应该完全负责建立好可预期的服务拓扑!

还有一个比较重要的问题是测试代码运行时所处的网络环境也必须是确定的。有一类错误的情况是被测服务被部署在了 IDC,开发者的流水线 runner 容器却是在 devnet,导致测试时发出的请求根本到不了被测服务。这种情况开发者应该根据流水线平台提供的工具,保证流水线 runner 处在正确的网络环境之下。

我有见过一些用例中使用随机数作为文件名参数来调用 “创建文件” 接口,有时接口返回成功,有时返回失败,返回失败是因为数据库中该文件已存在,而业务逻辑中是不允许文件重名的 (例子: 文档团队用例 TestFoldersCreate,创建随机名称的文件夹,预期成功,实际文件夹名冲突)。此种是典型的没有正确 setup,或者更严重地说用例作者自己根本没想清楚它应该有两个用例来分别测试接口的不同场景。

我见过的 E2E 测试中有许多从某个 “测试数据管理系统” 里面拿 token 来作为接口测试的 token 的,然后测试经常因为 “测试数据管理系统” 返回的 token 并不是一个有效的 token,此种情况也属于没有正确地 setup,测试时应该使用确定的输入参数,期望确定的响应结果。否则就会导致用例随机地失败或成功。
此外,还应该考虑同一个用例被不同流水线同时执行时,当前准备的测试数据会否影响断言,不同用例同时执行时,是否用到了相同的文件 ID 从而导致的互相干扰。
总之,setup 是非常重要的,它保证了被测系统处在一个确定的,可预期的状态。对一个状态不确定的系统发出请求然后断言它给出预期的响应基本上是在碰运气,那不是自动化测试该有的行为。

这里有个特殊的例子 TestDriveAPIPinFile,该用例测试的功能是 pin 住文件,所有接口请求返回后只断言了 error 非空,但是如果目标服务只提供空的,返回 200 的 http 接口,也能顺利通过这个测试。建议的做法是通过相应的查询类接口,判断是否真的 pin 住。

上述 TestFoldersCreate 作了删除文件夹动作,TestDriveAPIPinFile 做了取消置顶的操作,这些是非常正确的做法。反例有 TestAddDoc, 用例中进行了 "doc_add" 操作,在结尾没有进行 "doc_delete" 操作。

3.对测试自动化工具的要求

像 E2E 这种大颗粒度的自动化测试是很难的,要求非常多,测试自动化工具的及格线就是要能提供全部的功能去帮助测试环境的建立,驱动测试,收集测试数据等等。这些工具有的是流水线平台提供的,有的是基建部门如 docker 平台提供的,有的是测试工具组提供的。测试者在一次测试的流程中可能到各个平台中去操作,后台测试自动化工具组的一个目标就是为测试者提供便捷的手段去操作这些平台,将测试输出的消息以推送的形式提供给测试者,尽可能地避免测试者去 “拉取” 信息的场景,提高测试者的效率。

自动化测试工具的另一作用是聚合测试结果,通过对一段时间的测试结果分析/聚合,可以给业务团队提供一些有价值的分析反馈。

4.对测试者的要求

每一个测试用例的价值都是是用例作者生产的,并且用例作者也是这个价值最主要的收益方。这看似一句废话,其包含两个方面:

测试知识也包含两方面,测试理论和对业务系统的了解。测试理论上解决问题的工具和方向,对业务系统的理解就是对问题本身的理解,优秀高效的工程师这两者缺一不可。本文泛泛而谈,希望能给对自动化测试感兴趣的同学一点点信息。

优测云测试平台:是一个为企业与开发者提供专业的测试工具和服务的平台,沉淀十年产品测试经验,提供终端测试、接口测试、性能测试、安全测试等多领域测试服务与产品,协助客户提高效率降低成本,保证产品质量。


↙↙↙阅读原文可查看相关链接,并与作者交流