验收测试让交付团队超越了基本的持续集成,即验证应用程序是否为用户提供了有价值的功能。不过对于刚开始尝试部署流水线的团队来说,想要自动化验收测试,需要跨过三大门槛。
一是实现和维护验收测试的技术门槛。理想情况下,验收测试最好可以模拟用户与应用程序的真实交互,因此如果有图形界面的话,验收测试理应通过这个界面和系统打交道。然而,直接通过 GUI 进行测试会遇到几个问题:界面变化速度很快、场景的准备相对复杂、拿到测试结果较难等。比如一个典型的 WEB 应用程序,如果通过 GUI 测试,那么一般需要解析 HTML 标签来填写参数,提交表单,最后再次通过解析来获取系统的返回值。如果测试代码中充斥着操作 HTML 的细节,测试的可读性就会大大下降,验收测试本身也更脆弱,在需求变更时反而会拖慢进度。
二是交付团队工作方式的变化。在传统团队中,需求分析、开发和测试是独立而又顺序的过程。就算能形成详细的需求文档,三方对同一段文字可能都有自己的理解。结果经常出现偏差,需求分析人员抱怨开发人员没有正确理解需求文档,开发人员抱怨需求文档不清晰、抱怨测试人员故意挑刺。敏捷实践和验收测试的出现缓解了这一问题,通过预先定义验收规格,减少文字上的误解,明确了开发工作的完成标准。不过这种思维方式的转变很难一蹴而就,需要交付团队及其利益关系人共同持续努力才能成功。
三是对组织的环境、配置管理及部署流程的挑战。当引入自动化验收测试后,对整个部署流水线的自动化程度会有更高要求。比如部署流水线应该能够自动将应用程序部署到待测试的环境中。如果应用程序依赖数据库,那么还应该能够部署数据库 schema。另外一些运行时配置也需要通过脚本完成设置。这当中除了脚本准备之外,组织的环境管理也是要能跟上的。一般情况下,稍微大一些的组织都是有专门的运维团队(而非交付团队)来管理硬件设备和其配置的。因此,这个问题一般也涉及多个团队来协作解决。
面对这三座大山和进度压力,新手团队可能会感慨“信息量略大”而止步不前。这时不妨考虑各个击破,三个问题中的工作方式转变涉及的利益干系人最多,难度也最大;环境管理问题虽然涉及不同团队,但一般还是技术部门内的问题,关起门来好商量;验收测试的实现 / 维护主要是技术问题,相对最简单。如果时间和资源确实有限,不妨考虑牺牲一部分验收测试的有效性,采用简单的非端到端验收测试,在自动化部署流程方面也可以做一些折中,集中力量转变工作方式。当整个工作已经进入节奏,再去改进某个具体环节时就顺利很多了。团队只要愿意迈出一小步,也能获得很大的价值。
过渡方案:相对简单的非端到端验收测试
如果团队的技术积累还不足,又没有足够的资源,不妨考虑简单一些的验收测试策略作为过渡方案。非端到端的验收测试是指直接调用应用程序内部的逻辑结构来驱动测试。由于测试代码和产品代码都使用同一种语言编写,可以省去比较繁琐的数据格式解析。而在准备测试数据和场景时,直接调用内部逻辑块一般也更方便。以典型的使用 SpringFramework 的 Java WEB 应用程序为例,团队可以采用和集成测试类似的基础架构来编写非端到端的验收测试。
这里所说的集成测试的目的是验证应用程序与外部服务的连接能否正常工作。这与应用程序实现的具体功能关系不大,因此一般只加载必需的 ApplicationContext。
图表 1 集成测试
非端到端的验收测试可以采用和集成测试一样的测试基础架构,这样你就可以使用熟悉的测试库了,不同的是需要加载整个 ApplicationContext 以尽可能模拟应用程序被部署后的情况。
图表 2 加载整个上下文
由于可以访问整个 ApplicationContext 中的任一对象,我们可以通过访问应用程序的内部组件来执行测试,比如应用层的某个 Service。但需要注意的是,选取的组件离 UI 层越远,其模拟真实用户交互的有效性就越差,而且受内部实现变更的影响越大。如果应用程序使用 spring-webmvc 的 3.2 以上版本,推荐使用它的 mvc 测试库。spring-test-mvc 提供了类似 http 请求的 DSL,此时虽然测试还是基于 ApplicationContext,但并不直接访问内部组件了。这个方案对于新手团队比较友善,但请注意,这仅仅是个过渡方案,因为:
- 非端到端测试无法提供全面的回归测试,尤其是 UI 操作。在好几个项目中,我们发现仅采用非端到端测试覆盖的功能,团队不得不保留手工回归测试。如果 UI 上包含了大量复杂的控制逻辑甚至有业务逻辑泄漏到 UI 组件中,这会稀释验收测试带来的收益。
- 由于测试加载的 ApplicationContext 和 Web 容器加载的 ApplicationContext 存在差异,非端到端测试可能会漏掉一些问题。比如在非端到端测试中一次性加载了 booking-servlet.xml 和 root.xml,使他们成为了一个整体的上下文,而实际上在 Web 容器中并不完全是这样,root.xml 中的 bean 并不能访问和控制 booking-servlet.xml 中的 bean。一个常见问题就是如果在 booking-servlet.xml 中需要使用占位符,而恰巧我们已经在 root.xml 中有一个现成的 context:placeholder/ ,看起来水到渠成,而且在测试中也没有问题,但实际部署到 web 容器时,就会加载失败。
- 非端到端的验收测试不能作为任务完成的最终标准。因为还有 UI 部分还没有完成。当这类验收测试通过时,我把这个任务称作“可以进入 UI 调试的”。
因此,如果团队有足够的技能和资源时还是应该直接使用端到端的验收测试,尤其当应用程序提供 API(比如 WebService)或是采用更易于解析的数据格式与客户端交互时。比如如果应用程序提供了基于 JSON 的 API,完全可以使用 http-client 来驱动测试。
实现非端到端的验收测试
来看看第一个验收测试,这个案例来自于著名的 dddsample ,为了让验收测试能够看上去高端大气上档次,我们将使用 Cucumber 来组织验收测试。验收场景描述的是业务员如何登记航运货件并解释了登记完成后货件的各项状态。
图表 3 第一个用户故事及其验收场景
Cucumber 提供了一系列的 Annotation 来帮助我们验收场景文本与测试代码粘连在一起。
图表 4 实现验收测试 -1
图表 5 实现验收测试 -2
接下来,当运行测试时,你就可以得到一份漂亮的 html 报告
图表 6 测试运行入口
维护非端到端的验收测试
当团队开始编写验收测试之后,一般没过多久就会发现验收测试的开发进度越来越慢,而且有时遇到测试失败,但其实应用程序并没有缺陷的情况。验收测试对代码质量的要求也很高,相比单元测试,为了要达到测试所需的起始状态,验收测试的准备工作要更复杂。而且由于需要解析应用程序返回的数据,验收测试的断言也会更加琐碎。因此,团队最好尽早开始重构验收测试,下面的建议或许有用处:
- 建立最小测试数据集并且尽可能隔离测试的数据。有时团队会发现两组测试由于依赖同一批数据而产生冲突,单独执行任一组测试都能通过,但一起执行就会失败。比如在示例代码中,对于不同的货件处理事件登记场景,验收测试都会注册一个新的货件。
- 隐藏断言细节。这样可以减少重复代码,并提升测试的可读性。把琐碎的解析逻辑隐藏在领域语言编写的方法中。
例如:如果多个测试用例都会对货件的运输状态进行断言,可以把解析细节提取出来,这样可以去除重复代码,并且减少语法噪声。
图表 7 抽取断言
- 尽可能使用已实现的功能来实现测试场景的准备。有一些步骤可能是多个测试用例都需要来准备数据的,可以把此类步骤抽取出来。这样也可以减少重复的代码,当应用程序随着需求变化时,验收测试会有更强的适应性,而且抽取出来的方法由于隐藏了技术细节,使用起来更简练。直接使用数据脚本的方案看起来很诱人,但一旦内部结构改变,数据脚本也得跟着改。
图表 8 抽取公共步骤
验收测试对实践部署流水线的团队有着重要意义,也是很大的挑战。希望大家都能找到合适自己的方法。最后介绍几个有用的测试库。
- Moco ,当有外部系统集成需求时,集成测试和验收测试的一大利器。在示例代码中你可以找到一处例子。
- GreenMail ,如果应用程序需要发送邮件的话,它可以提供一臂之力。不过在部署流水线上的端到端验收测试中,由于一般应用程序和测试并不运行在同一台机器上,很难对邮件进行直接的断言。这时一种方案是修改应用程序的架构,把发送邮件的实现分离到一个专用的应用中去并使用消息队列集成。那么在验收测试中,我们就可以通过监听对应的消息队列来断言了。
- Awaitility ,在需要对异步处理进行断言时有所帮助。
作者简介
周宇刚是一位乐于磨练技艺的开发者。他的研究和兴趣包括 IT 架构、领域驱动设计和敏捷实践。
感谢崔康对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论