一、前言
本文作者介绍了什么是E2E测试以及E2E测试测什么,并从对于被测系统、测试用例、测试自动化工具、测试者四个方面的要求,介绍了如何保证E2E测试有效性,干货满满,值得学习。
二、什么是E2E测试
相信每一个对自动化测试感兴趣,并且付诸实践去了解测试理论的人可能都听说过“测试金字塔(Test Pyramid)”。 测试金字塔的核心理念是分层测试,即按照不同的隔离粒度来测试。例如我们可以将一个项目看作是一个个方法/函数组成的,那么可以对每个方法/函数进行测试,这大概是目前测试的最小隔离粒度了,通常称之为单元测试。我们也可以将一个项目看作是一个个模块/服务组成的,对每个服务进行测试,这里的隔离颗粒度就稍大。然后各个服务之间串联起来,最终只有一个用户界面暴露给用户,从这个用户界面上发出的服务请求,到用户界面收到响应做出变化,数据流经的整个链路我们称为端到端,以这样的场景为测试颗粒度成为端到端测试。
测试金字塔定义了分层测试理念,但是具体每一层是什么却没有定论,下图是 Mike Cohn 最初提出测试金字塔这一概念时的样子,但是实践中大家的分层方法各有不同,但是不变的是:隔离颗粒度越小,在这一层的测试用例就应该越多,每个测试用例自身也应该越小,运行速度也越快。
传统的端到端测试往往是从用户界面出发,测试整个技术栈,但是随着客户端种类越来越丰富,前端框架越来越成熟,UI 可以单独写单元测试了,而可以把“从发出的后端 API 请求,到接收到响应”这一段作为端对端的测试主题。
三、E2E测试测什么
E2E 测试旨在复制真实的用户场景,以便可以验证系统的集成和数据完整性。从本质上讲,测试会遍历应用程序可以执行的每个操作,以测试应用程序如何与硬件、网络连接、外部依赖项、数据库和其他应用程序进行通信。
当你写 E2E 用例时,想象你就是用户,用户在使用某个功能时,第一步要做什么,第二步要做什么,每一步期望会得到什么样的结果。E2E的宗旨就是保证用户使用我们的系统可以得到他想要的功能。
不要在 E2E 测试中测试异常流,比如下游服务失败,数据库超时等等情况。这些异常流应该在更小颗粒度的测试中去做,比如单元测试或者接口测试,事实上在小颗粒度的测试中也更容易做这些测试。
四、如何保证E2E测试有效
1.对被测系统的要求
不是任何代码都可以被自动化测试的。这一点对单元测试和大型测试都是适用的。对于单元测试,可测的代码需要抽象良好,解耦合理,这一点是很好理解的。对于大型测试,被测系统的要求更加复杂,一个最核心的要求是被测服务要具备可观测性(Observability). (软件的可测试性要求有许多提法,但是所有的提法里一定有可观测性这一指标)。系统的可观测性是指系统通过其接口能暴露系统的内部行为或状态.
- 状态码和状态消息是最有价值的观察点
业务返回的状态码和状态消息是给“客户端”看的,它能直观地反映当前服务失败的原因以及用户流程是否可以恢复。比如当错误码是用户 token 失效,则客户端可以提示用户重新登录来恢复用户流程,当错误码是系统内部服务暂时不可用,则客户端应该提升客户稍后再重试,在内部服务恢复前重试一定还是失败。
为了表述的方便我们先举一个例子。一个优惠券业务系统中,某一类优惠券只发放给高级会员,同时要求买满1000指定类目商品才能使用。现在考虑“在订单中使用优惠券”这一接口。这一接口的入参有用户信息,订单信息和优惠券的 ID。这个接口的调用流程如下:
其中 3.订单是否满 1000 是当前服务的内部逻辑,不是服务调用。1 和 2 均是对下游服务的调用。
-
任何一个接口返回的状态码应该是收敛的,或者说是可枚举的。上面例子中虽然下游服务并不复杂,但是系统的具体失败点可以有非常多,仅以会员服务的调用为例:a. 调用会员服务超时,b. 名字服务中找不到会员服务,c. 会员服务返回用户非高级会员,d. 会员服务返回用户未注册, e. 会员服务返回 token 过期。类似的失败点在商品服务中也会有,当前服务的下游依赖服务越多,具体的失败点也就会越多,直接下游服务数量会增加失败点的常数量级(加法关系),而间接下游服务的数量会增加失败点的几何量级(乘数关系)。我们不可能把下游暴露的错误原原本本地透传到客户端去。
-
接口应该按照用户可选择的行为来归类错误。以上面会员服务调用引起的失败为例,面对失败用户无非有 3 大类选择:
- a. 重试有概率能成功,客户端可以提示用户重试. 此类错误适用于系统暂时不可用,但是有概率在一定时间后自动恢复的情况,如下游由于过载导致的请求丢弃,重发请求有可能被接受。
- b. 短时间内重试一定不会成功,过一段时间重试有可能成功,客户端可以提示用户稍后重试.此类适用于系统错误,比如名字系统中找不到会员服务的地址,有可能是会员系统整个挂掉了,在会员域的同事重新部署完毕之前不论怎么重发请求都不会成功。实践中此类错误在 UI 上的展示可能跟 a 类错相同,但是其内在逻辑上是不同的,追求优秀的用户体验应该使用不同的 UI 展示。
- c. 重试不可能成功, 必须进行前置动作然后才可能重试成功。本例有两个具体的前置动作: 登录后重试或升级为高级会员后重试。
综上,会员服务引起的失败点可以归结为 3 类,4条。实践中可以将某一类错误使用固定的状态码段,具体错误使用不同的状态码值。但是,随意地使用错误码是非常错误的行为,拿到错误码不知道哪里错。以上我们列举了 5 个失败点,但是归类到了 4 个错误码,这是状态码收敛的过程。
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
- Setup 阶段是 flakiness 的常见来源。个人经验这是很多初涉自动化测试的同学最容易出问题的地方。在 Setup 阶段需要保证被测系统处在一个确定的状态,想象被测系统是一个状态机,我们的测试内容是在某个特定状态下,给系统一个输入,保证其进入到我们期望的状态,并/或给出我们想要的输出。当然那种纯“无状态”系统可以认为是只有一种状态的状态机。广义的 setup 包括测试 binary 执行前的流水线前置准备步骤,和单个用例中准备输入参数的代码调用。举个例子说明 setup 阶段我们要做什么:一个“删除文件”的接口,接口的参数有用户 token,用户要删的文件 id,被测的系统有多种状态,用户 token 是否过期,文件是否存在,文件存在的情况下该用户对该文件是否有删除的权限等等。每一个可能的状态的排列组合都应该有一个用例去测试。假设其中一个用例是期望已登录用户对没有权限的文件进行删除时,接口报错 “Permission Denied", 那么在 setup 阶段我们要做的事情有:
- 保证系统已经被正确地部署,这通常是流水线上的某个步骤。
- 拿到一个没有过期的用户 token, 不管用什么手段,你拿到的 token 一定要是有效的,未过期的。
- 拿到一个该用户没有权限的文件 id,不管用什么手段,你必须保证拿到的文件 id 是存在的,且该用户一定上对它没有删除权限的。
准备好了这些我们才能进入下一个步骤:调用接口( invoke)。上面的表述中 “一定” 和 “必须”就是核心。下面列举几个实践中容易犯错的情景:
在 E2E 测试中,正确部署往往被误解为只是把被测服务部署到正确的环境里,其实这里还要求所有被测服务的下游服务也必须处于可预期的正确状态。我经常听到有人说 E2E 测试因为下游服务而导致的失败,然后就不管了,这是不对的,测试者应该完全负责建立好可预期的服务拓扑!
还有一个比较重要的问题是测试代码运行时所处的网络环境也必须是确定的。有一类错误的情况是被测服务被部署在了 IDC,开发者的流水线 runner 容器却是在 devnet,导致测试时发出的请求根本到不了被测服务。这种情况开发者应该根据流水线平台提供的工具,保证流水线 runner 处在正确的网络环境之下。
我有见过一些用例中使用随机数作为文件名参数来调用“创建文件”接口,有时接口返回成功,有时返回失败,返回失败是因为数据库中该文件已存在,而业务逻辑中是不允许文件重名的(例子: 文档团队用例TestFoldersCreate,创建随机名称的文件夹,预期成功,实际文件夹名冲突)。此种是典型的没有正确 setup,或者更严重地说用例作者自己根本没想清楚它应该有两个用例来分别测试接口的不同场景。
我见过的 E2E 测试中有许多从某个“测试数据管理系统”里面拿 token 来作为接口测试的token的,然后测试经常因为“测试数据管理系统”返回的 token 并不是一个有效的 token,此种情况也属于没有正确地 setup,测试时应该使用确定的输入参数,期望确定的响应结果。否则就会导致用例随机地失败或成功。
此外,还应该考虑同一个用例被不同流水线同时执行时,当前准备的测试数据会否影响断言,不同用例同时执行时,是否用到了相同的文件ID从而导致的互相干扰。
总之,setup 是非常重要的,它保证了被测系统处在一个确定的,可预期的状态。对一个状态不确定的系统发出请求然后断言它给出预期的响应基本上是在碰运气,那不是自动化测试该有的行为。
-
- Invoke 就是对被测接口的调用,这一步通常不会有什么问题。
-
- Assert 是断言期望的返回结果。正如前言所述,接口的返回状态码和状态信息应该是收敛的,对于那些反映正常用户流的状态码,每个状态码都要有至少1个断言,否则测试就是不完全的。实践中我见过很多对接口的返回仅仅断言了状态码 OK,而对返回的数据负载缺乏有效的断言,这在那些数据负载不重要的情况下是没有问题的,但是对于有数据负载的接口中,对这些数据对断言才是 E2E 测试的重点。
这里有个特殊的例子TestDriveAPIPinFile,该用例测试的功能是 pin 住文件,所有接口请求返回后只断言了 error 非空,但是如果目标服务只提供空的,返回 200 的 http 接口,也能顺利通过这个测试。建议的做法是通过相应的查询类接口,判断是否真的 pin 住。
-
- Teardown 阶段在 AAA 和 Given-When-Then 的提法中被忽略了,但是它也是非常重要的。对于诸如“创建文件”这样的接口,并且有业务规则规定了文件不能重名,在成功地创建了文件之后,也应该删除掉该文件,否则下次运行,同样的用例同样的输入却得到失败的结果。(假如每次测试都在沙盒中(使用不同的数据库/文件系统),测试完成后所有内容都被丢弃,那么这里的 Teardown 缺失也不会引发问题,但还是建议在用例中做一下清理,不作清理的话就会限制这个沙盒中每个用例仅能运行一次,这在有 flaky 检测行为的测试活动中是不能接受的)。
上述 TestFoldersCreate 作了删除文件夹动作,TestDriveAPIPinFile 做了取消置顶的操作,这些是非常正确的做法。反例有TestAddDoc, 用例中进行了 “doc_add” 操作,在结尾没有进行 “doc_delete” 操作。
3.对测试自动化工具的要求
像 E2E 这种大颗粒度的自动化测试是很难的,要求非常多,测试自动化工具的及格线就是要能提供全部的功能去帮助测试环境的建立,驱动测试,收集测试数据等等。这些工具有的是流水线平台提供的,有的是基建部门如 docker 平台提供的,有的是测试工具组提供的。测试者在一次测试的流程中可能到各个平台中去操作,后台测试自动化工具组的一个目标就是为测试者提供便捷的手段去操作这些平台,将测试输出的消息以推送的形式提供给测试者,尽可能地避免测试者去“拉取”信息的场景,提高测试者的效率。
自动化测试工具的另一作用是聚合测试结果,通过对一段时间的测试结果分析/聚合,可以给业务团队提供一些有价值的分析反馈。
4.对测试者的要求
每一个测试用例的价值都是是用例作者生产的,并且用例作者也是这个价值最主要的收益方。这看似一句废话,其包含两个方面:
-
- 作为用例作者,你做测试的目的是什么?是仅仅为了完成上级交待的任务,必须要有 E2E测试?是为了 EPC 覆盖率达标?如果不认为自己是测试的收益方,那用例质量想必不会太好。我们应该通过测试来最大程度地保证系统的稳定,谁都不想在半夜,在休假时被系统告警打扰,也不想随便改一点代码逻辑就要手动重复一堆重复过无数次的用户逻辑,完了还没什么信心保证测过的是真的没问题(true-positive)。
-
- 用例作者需要有足够的测试知识,再强大的工具也不能自动帮你写出完美的测试,你的用例能为你提供多少价值,取决于你的用例质量,用例质量取决于你的知识水平,付出越多收获才能越大。
测试知识也包含两方面,测试理论和对业务系统的了解。测试理论上解决问题的工具和方向,对业务系统的理解就是对问题本身的理解,优秀高效的工程师这两者缺一不可。本文泛泛而谈,希望能给对自动化测试感兴趣的同学一点点信息。
优测云测试平台:是一个为企业与开发者提供专业的测试工具和服务的平台,沉淀十年产品测试经验,提供终端测试、接口测试、性能测试、安全测试等多领域测试服务与产品,协助客户提高效率降低成本,保证产品质量。