自动化验收测试是持续交付测试策略十分关键的一环,它为开发者们洞察系统行为提供了一个重要而不同的视角。Dave Farley 提出,保持验收测试持续运行和通过率是开发者们的责任,而不能依靠另外的 QA 团队去维护验收测试,导致拖累开发团队的进度。
独立软件开发者和顾问 Dave Farley 在 Craft 2017 大会上讨论了持续交付中的验收测试。InfoQ 通过问答、总结和文章的形式对大会进行了报道。
Farley 说,好的验收测试可以看做系统行为的“可执行规范”。为了提高可读性,他建议开发者使用领域相关的语言编写测试,这样我们就更容易地向产品负责人、客户或是其他人解释测试的意义。另外,它还能让测试用例变得容易维护。
验收测试中的时间依赖性问题有两种解决方案。开发者可以选择忽略掉测试用例中的时间因素,但这就会导致某些情况无法被测试到,错过某些错误。另外的选择是把时间因素作为外部依赖。Farley 给出了一个如何控制时间因素的例子。例子中,他构造了一个可以提供时钟功能的桩(stub),为测试框架增加了操作时间因素的相关函数。
InfoQ 向 Dave Farley 提出了如下问题:是什么让验收测试变得如此之难,我们又如何做好验收测试?
InfoQ: 开发者应在什么情况下使用验收测试?验收测试的目的是什么?
Dave Farley: 在我看来,自动化验收测试是持续交付测试策略十分关键的一环。结合低层次的单元测试 — 最好是作为测试驱动开发(TDD)的成果 — 自动化验收测试可以向开发者们提供一个重要而不同的视角来观察系统运行行为。
验收测试可以从一个外部用户的角度,在近似于生产环境的测试环境中对系统进行评价。测试用例最好写成系统行为“可执行规范”的形式。
最后,在提供系统可执行规范视图、为用户提供功能性合约准则之外,验收测试还向开发者提供了自动化的功能完成定义(definition of done),还是演习自动化部署和系统配置的最佳机会。
InfoQ: 是什么让验收测试变得这么困难?
Farley: 耦合度!大多数组织没有努力将高层次功能测试用例与需要运行测试的系统进行解耦。因此,每当承担测试的系统发生变更时,测试用例就会失效,需要跟进修改。我的演讲主要就是围绕着这个问题展开的。为了达到目标,开发者可以引入几个技术,如领域特定语言(Domain Specific Language),它允许开发者把验收测试定义为“可执行规范”,而不仅仅是“测试”;测试隔离,让开发者撰写测试时,专注于系统的行为是“什么”,而不去考虑系统“如何”实现。这些技巧可以极大地帮助开发者,高效地维持测试用例(也就是系统规范)与需要被测试的系统之间的关注点分离。
在解决验收测试问题的实践中,我还学习到解决问题的关键还在于把责任交给正确的人。业界普遍存在的一个反模式,就是让独立的 QA 团队拖累开发团队,即使他们努力地实现测试的自动化。
如果开发者们能成功定义系统行为真实可用的“可执行规范”,那么当代码改动导致了测试用例失效,解决问题责任就落在了开发者自己身上。毕竟,一旦系统的改动导致测试失效,那是因为系统不能再满足功能规范(验收测试套件所描述的那些东西)。任何人都可以写测试,但开发者自己应当在测试首次可执行之后,就承担起保证测试可以正常运行的责任。
InfoQ:如何隔离不同的测试用例?
Farley: 我认为测试用例隔离有三层重要的层次:系统级别隔离,功能性隔离以及临时性隔离。
系统性隔离意味着开发者需要明确定义被测试系统间的边界。开发者需要精确地控制被测试系统的状态。为了达到这个目标,开发者应为被测试系统的边界撰写测试用例。在测试目标系统时,开发者不希望与上游系统进行交互,因为这意味着开发者将会对测试边缘情况失去足够的控制。开发者也不希望收集下游外部系统的结果。开发者希望做到的是,直接通过系统提供的任意形式的普通接口调用系统的行为,用桩(stub)替代外部依赖,以便收集系统的运行结果、设定正确运行时所期望的数据结果,或为引发系统行为注入相应数据。
通过功能性隔离,开发者可以使用存在于多用户系统中的天然边界,隔离不同的测试用例。开发者可以利用功能性隔离共享大型复杂系统的启动开销,同时使多个的测试用例在互不影响的情况下并行执行。原理十分简单,在测试用例的设置阶段,我们会创建新账号、定义产品和市场(或是可以代表系统内天然功能性边界的任何概念),并在单独的测试用例的上下文中使用。举个例子,如果是我为亚马逊网上书店编写测试用例,那么我会在每个测试用例开始前注册一个新账号,并创建一本可供售卖的书。
临时性隔离允许开发者在重复运行同一个测试的时候,都能得到相同的结果。再次说明,我不想在每个测试用例的最后清除系统资源,这样做的开销太大。我希望在同一个已部署系统中运行同一个测试用例。为了达到这个目标,我们使用刚才描述的功能性隔离方法,再结合使用命名代理(proxy-naming)技巧。当测试用例请求创建一个新账号或是一本书时,测试框架介入其中,为将要创建的新项目构造一个别名,而不是使用测试用例中的名字。在测试的作用域内,测试用例还是可以使用测试用例中的名字,但框架内部会将其映射为唯一的别名供测试使用。利用临时性隔离,开发者在重复运行同一个测试的同时,还能享受到功能性隔离带来的好处。
InfoQ:你建议开发者不应该使用“UI 录制回放”系统。可以解释下原因吗?它的替代品是什么?
Farley: 我希望可执行规范应专注于用户行为,而不是专注于用户界面的实现细节上。
如果测试用例被编码成诸如“placeOrder”(下订单)或是“payForPurchases” (支付交易)等等问题领域内的术语,我就可以为任意的系统界面使用同样的测试用例。假设系统提供了图形用户界面和 REST API 两种方式下订单,就能为两种通讯通道创建同一个测试规范,从而达到实现关注点分离的目标。如果能做到这一点,开发者可以更容易地应对相对较少的系统改动,比如重写 Web UI,使得已有的测试用例(可执行规范)仍能得到重复使用。
本质上,在 UI 录制回放系统中,测试的关注点是 UI,而不是用户所期望的系统行为。这也就意味着 UI 录制回放系统的测试关注点在于技术本身,而不在于系统行为。这就导致这样的测试用例总是很脆弱的,并且更容易由于目标测试系统中相对较小的改动而失效。因为从长远来看,它会带来很多额外的工作,因此我会避免使用这种测试。
InfoQ:如何让测试用例和验收测试变为更有效率的整体?
Farley: 效率在测试的每个环节都需要普遍保持。开发者应思考如何在正确的地方测试正确的内容。
我不希望在验收测试中为每行代码编写测试用例,这样的代价太大了。我会在 TDD 中编写测试用例,并期望更早地获得反馈结果。
我不希望为测试使用生产环境的数据,因为这样的数据太过庞大,还会降低测试环境的启动速度和测试用例的执行速度。我希望使用最小测试数据集,这样我才能更精确地定位到需要测试的行为。
我会针对应用中不常见的操作进行优化,比如账号注册等。这可以提高测试和应用的启动速度,我因此可以更快地得到验收测试的结果。
我会尽量避免在测试用例中使用睡眠或是等待命令。开发者经常会把这些命令当做狗皮膏药般隐藏竞态条件(race-condition)问题。它们会让测试用例变得低效,而且经常也没真正地解决竞态条件问题,只是把问题转移到了别的地方。
验收测试是高品质测试策略的重要部分,但不是唯一内容。它不应当替代 TDD,或是替代 TDD 中定义的底层单元测试。验收测试应当作为 TDD 重要的补充。
对于最简单的项目,我依然会为其使用最小化的开发流水线。这样的流水线包含如下内容:“提交测试”(TDD),“验收测试”(可执行规范)和“自动化部署到生产环境”。
查看英文原文: Automated Acceptance Testing Supports Continuous Delivery
感谢薛命灯对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论