6.3 照管好你的测试
自动化特性的好处在于你可以把它们作为活文档来长期信赖,因为你会将每一个场景都用于检查产品代码,以确保它们仍然有效。对于同代码打交道的程序员来说,这还有另一件好处:在他们开发系统的时候,那些测试可以充当安全网,对任何破坏已有行为的错误都给出警告。
因此,你的特性可以充当一种反馈机制,对整个团队来说提供关于系统行为的反馈,对程序员来说还能提供是否破坏已有行为的反馈。想让这些反馈循环带来好处,测试需要执行迅速,还需要可靠。我们首先来看看影响测试可靠性的问题。
6.3.1 渗露的场景
Cucumber的场景从根本上讲就是状态转换测试:你将系统置于给定的(Given)状态A,执行动作X(When),然后(Then)验证它迁移到了期望的状态B。因此,每个场景在开始运行之前都需要系统处于某种确定的状态,而每个场景结束时也要把系统置于一种脏的新状态。
如果在两个测试之间没有重置系统的状态,我们就说状态在测试之间发生了渗露(leak)。这是导致测试脆弱的主要原因之一。
如果一个场景需依赖之前另一个场景留下的状态才能通过,就说明你在两个场景之间制造了依赖。如果有一连串的场景像火车车厢一样彼此依赖,那么离火车事故就为时不远了。
如果第一个场景,也就是正好按照下一个场景的需要将系统状态准备好的那个场景,发生了变化,那么突然之间后面那个就要失败了。即使没有改动前面一个,可如果你只想单独运行第二个场景,那又会怎样呢?没有了前一个场景渗露下来的状态,它还是会失败。
这种情况的反面,即独立的场景,可以确保场景将系统置于干净的状态,然后再在上面添加自己的数据。这使场景能够自给自足,而不是跟其他测试留下的数据或共享的固件数据耦合到一起。投入些时间精力来构造一个良好可靠的测试数据构造器的库,可以更容易达到独立场景的目标。
独立的场景对于成功自动化测试的重要性怎么强调都不为过。除了独立设置自身数据的场景所带来的附加可靠性之外,它们读起来、调试起来都更加清楚。当你仅靠阅读场景就能准确地看清它所使用的数据,而不需要在固件数据脚本中甚至更麻烦地直接在数据库中四处查阅时,理解或诊断一次失败就容易多了。
测试数据构造器
如果你使用Ruby,那肯定熟悉FactoryGirla这个gem。FactoryGirl是测试数据构造器(Test Data Builder)b这一模式的出色实现。如果你还不太了解,以下内容简单概括了它的好处。
假如你正在测试一个工资单系统,作为某个场景的一部分,你需要创建一条PayCheck记录。按照你的领域模型结构,PayCheck需要一个Employee,而Employee需要一个Address。每种结构还有其他一些必要的字段。你不需要在步骤定义代码中分别创建所有这些对象,也不必维护一大堆固件数据,只需要这样做:
Given /^I have been paid$/ do
Factory :pay_check
end
基于你的数据模型,只要用模型的结构配置好FactoryGirl(配置的细节参阅FactoryGirl文档),然后你只需要向它要一个PayCheck,FactoryGirl便会创建好PayCheck记录以及它所依赖的所有对象,并用适当的默认值设置好那些必要的字段。如果你喜欢让某个字段拥有特定的值,可以让FactoryGirl把默认值覆盖掉:
Given /^I have been paid 50 dollars$/ do
Factory :pay_check, :amount => 50
end
当数据的创建可以如此方便时,你就不再需要走到哪里都拖着一大包固件数据。当然,创建这样的构造器需要一小笔前期投资,但很快就会取得回报,那就是可靠的、可读的场景和步骤定义代码。如果你的团队不使用Ruby也不要紧,只需要很少的额外工作,你仍然可以让ActiveRecord和FactoryGirl指向其数据库。如果不行,你还可以针对自己使用的语言找找其他类似工具。
6.3.2 竞争条件和打瞌睡的步骤
给一个足够复杂的系统编写端到端集成测试时,你终归会遇到竞争条件和沉睡的步骤的问题。当系统的两个或多个部分并发执行,但执行是否成功需仰仗其中某一部分首先结束,这时就会发生竞争条件。就Cucumber测试来说,你的When步骤可能导致系统启动一些后台运行的工作,比如产生一份PDF或者更新一组搜索索引。如果后台任务碰巧在Cucumber
Matt说:fixture这一术语有不同的含义
在自动化测试领域,词语fixture至少有三种意思,这有时会引起混淆。在这一章我们使用术语固件数据(fixture data)表示用来给场景或测试用例设置上下文的数据。这是该术语在各种xUnit测试工具a以及Ruby on Rails框架中使用的最常见的意思。
有一种古老的传统(源于开创测试固件这一术语的硬件世界)是将测试系统与被测系统之间的连接称为fixture。这是“粘合代码”的角色,本书中我们称之为自动化代码(automation code)。FIT测试框架b用的就是该术语的这种含义。
一些单元测试工具(如NUnit)把水搅得更混了,它们把测试用例类本身称为fixture。关于通用语言,就说到这里吧!
执行Then步骤之前结束,场景就会通过。而如果Cucumber赢得了竞争,Then步骤在后台任务执行之前便执行了,那场景则会失败。
如果竞争双方势均力敌,你会遇上一个闪烁的场景,场景间歇地成功或失败。而如果一方胜算较多,竞争条件就会长期存在而不为人所知,直到某一天系统中某处新的变化平均了双方的筹码,场景便开始随机地失败了。
针对这种问题,一种粗糙的解决方法是在场景中引入固定时长的停顿或睡眠,从而为系统腾出时间来完成后台任务的处理。诊断竞争条件时这绝对是一种有用的短期技巧,然而,你还是应当抵住诱惑,一旦弄清了问题的原因就不要在测试中留下睡眠。引入打瞌睡的步骤不会解决场景闪烁的问题,只会让它发生的概率更低。同时,引入睡眠也会为测试的整体运行时间再增加额外的几秒,用一个问题换一个问题罢了。
如果一定要做选择,我们宁可要缓慢但可靠的测试也不要更快但不可靠的测试,不过我们没必要做这样的折中。当测试人员和程序员结对将场景自动化的时候,他们可以基于对系统工作原理的理解精心地编写测试。也就是说,他们可以利用系统中隐含的线索让测试知道何时能够安全前进,因而测试可以尽快地前进,而不必使用粗陋的定长睡眠。关于处理异步代码的示例及更多细节,可以参阅第9章。
6.3.3 共享的环境
在那些从手工验收测试体系转向使用自动化验收测试的团队中,共享的环境是我们经常发现的一个问题。按照传统方法,团队中的手工测试人员会使用一种称为系统测试环境的特殊环境,其中部署系统最近的构建版本。测试人员将在这一环境中运行自己的手工测试,并将bug报告给开发团队。如果有多名测试人员需要在同样的环境上运行测试,他们就需要彼此沟通,确保不会影响对方。
团队开始将测试自动化的时候,在一套新的环境中安装系统哪怕稍微有点麻烦,大家都很可能本着最小阻力的原则,把各自的测试脚本全弄到这套已有的系统测试环境中来。现在测试环境不仅在团队成员之间共享,也在测试脚本之间共享了。假设某一名开发人员收到了一个bug报告,想亲自重现一下,可他并未意识到自动化测试此时也在运行。作为bug重现步骤的一部分,开发人员无心地删除了自动化测试所依赖的一条数据库记录,自动化测试自然失败了。这种情形是导致闪烁的场景的一种典型情况。
对单一环境的共享使用还会对数据库这类炙手可热的资源造成沉重且不稳定的负荷,从而促成不可靠的测试。当共享的数据库超负荷运转时,正常情况下的可靠测试也会因超时而失败。
要解决这一问题,在新的环境中启动系统必须做到易如反掌。
6.3.4 被隔离的测试人员
测试人员在软件团队中常被视为二等公民。我们将在第 8 章中解释,开发一组健康的Cucumber特性套件不只需要测试技巧,也需要编程技巧。如果测试人员被晾在一边独自构建Cucumber测试,他们可能不具备让步骤定义和支持代码组织良好的软件工程技巧。不知不觉中,测试会变得一团杂乱,且脆弱得没人敢去改动。
为克服这一问题,编写步骤定义和支持代码时要鼓励测试人员和程序员协同工作。程序员可以向测试人员展示如何组织代码,使之结构清晰,如何提取可重用组件和库,以供其他团队使用。有些库,比如Capybara(参阅第15章),就是在程序员从团队的步骤定义中提取可重用代码时这样产生出来的。通过与测试人员结对,程序员对如何让代码可测试也会产生更深的理解。
一个团队如果把Cucumber用到好处,测试人员应该能够将运行基础检查的工作托付给Cucumber。这样他们自己就可以解放出来,去做更有趣、更有创造性的探索性测试(exploratory testing)工作,就像Agile Testing: A Practical Guide for Testers and Agile Teams [CG08]一书中阐述的那样。
6.3.5 固件数据
手工测试一套系统的时候,为它提供真实数据非常有用,这样你便如同在真实应用中使用系统。团队从手工测试转向自动测试的时候,你常常很想只移植产品数据的一个子集,从而让自动测试可以快速跟一个正常运行的系统交互。
一键式系统搭建
要避免因使用共享环境导致的闪烁的场景,团队需要一份脚本,做到只需按键一点就可以从零开始创建一份新的系统实例。
如果系统使用数据库,脚本产生的数据库应包含最新的模式(schema),以及所有的存储过程、视图、函数等。它应当仅包含系统正常运转所需的最少基础数据,比如配置数据。任何其他数据都应该等各个独立的场景自己去创建。
如果系统中有消息队列,或者memcache守护进程,搭建脚本也要启动它们,且使用你期望运行系统中应有的最低配置。
即使团队不在产品代码中使用Ruby,他们也可以试着用Ruby的ActiveRecorda gem来管理数据库模式和迁移脚本。ActiveRecord能让这类日常杂务变得轻而易举。
另一种方法,让每个测试构建自己的数据,看上去难度太大。在遗留系统中——特别是系统设计经不断发展而来的那种,创建当前测试所需的单个对象都意味着为它创建所依赖的全部对象构成的一整棵大树,你会觉得最方便的办法就是在固件数据(fixture data)中把它一次性创建好,然后与其他测试共享这棵大树。
这一方法有几个严重的问题。一组固件数据,即使开始时相对精简,时间长了体积也会只增不减。随着越来越多的场景开始依赖这些数据,且每个场景都要对它们做一点特别的处理,固件数据的体积会越来越大,复杂度也会越来越高。当你为了某个场景对数据做点改动,结果却使其他场景失败时,你将开始感受到脆弱的特性的痛苦。有这么多不同的场景依赖于固件数据,你会倾向于糊上更多的数据,因为这比修改已有的数据更加安全。某个场景失败时,你很难清楚系统中的哪一团数据可能跟失败相关,诊断起来也更加困难。
如果固件数据的集合体积庞大,在两个测试之间准备这些数据就会变慢。这会给你带来压力,诱使你编写渗露的场景,从而系统的状态不会在两个场景之间重置,因为这样速度更快。但这样做的后果我们前面已经解释过了。
我们认为固件数据是一种反模式(antipattern)。我们更倾向于使用之前介绍过的测试数据构造器,比如FactoryGirl1,那样可以让测试本身从内部创建相关数据,而不是让这些数据淹没在一大团混乱的固件数据集合中。
每日构建
当你碰到由大量场景所导致的缓慢的特性时,有必要考虑将构建拆成两部分。用标签来标记那些每次提交代码时都应运行的场景,其他的都降为只在每夜构建时运行。
这一模式的使用依赖于团队愿意承担风险的程度,以及犯错的趋向。降为每夜构建时运行的场景是那些极少失败甚至不失败的场景。它们针对的是那些几个月都不发生改变的功能,覆盖的是那些目前没有开发动作的稳定代码。它们是不得已时你情愿全部删除的场景。
为适当的场景打上针对签入构建的标签会带来额外的维护成本。一段时间后,这些场景中有一部分将趋于稳定,这时应该把它们降低到每夜构建中,然后用新的场景替换它们。
虽然每夜构建可以作为摆脱困境的好方法,通常来说长期的正确方案还是会打破你的大泥球。
6.3.6 大量场景
看起来似乎是陈述常识,可是拥有大量的场景是最容易导致特性整体运行缓慢的原因。我们并不是建议你放弃BDD并回到“牛仔式编程”(cowboy coding)当中,但我们真心建议你把缓慢的特性看做一个危险信号。除了需要很长时间等待反馈,拥有大量的测试还有其他的不利影响。数量巨大的特性很难组织得井井有条,阅读者也会觉得在其中前后翻阅极不方便。底层步骤定义和支持代码的维护同样变得更加困难。
我们发现,使用单一庞大构建的团队也更倾向于使用一种最适合用“大泥球”来描述的架构。由于系统的全部行为都实现在一个地方,所有的测试也只能放在一个地方,且只能作为一个巨块一起运行。在生存时间较长的Ruby on Rails应用中,这是一种典型的症状,这样的应用通常都经过长期持续的发展,各子系统间却没有清楚的接口。
我们会在下一节讨论更多关于如何处理“大泥球”的内容。正视这一问题,在端倪初现时一次性解决它至关重要,然而它并不是你在一夜之间就能解决的一个问题。
与此同时,你可以使用子文件夹和标签来保持特性的合理组织(参见第5章)。标签对此好处尤多,因为你可以用标签来拆分测试。你可以并发运行拆分后的不同测试集合,甚至把某些部分降为只在每夜构建(参见6.3.5节)时运行。
还有一种方法值得考虑,对于你在Cucumber场景中描述的行为,是否有一些可以下移一层,使用快速的单元测试来表达?热情拥抱Cucumber的团队有时会忘了还有单元测试这回事,而过分地依赖缓慢的集成测试来获取反馈。试着将Cucumber场景想象成向业务人员传达代码一般行为的粗略描述,同时仍然要靠快速的单元测试来获得尽可能高的覆盖率。实现Cucumber场景时让测试人员和程序员结对工作,从而协助达成这一目标。系统的某种行为需要用缓慢的端到端Cucumber场景来实现呢,还是应通过快速的单元测试来驱动呢?测试人员和程序员的结对可以对此做出恰当的决策。
6.3.7 大泥球
大泥球(Big Ball of Mud)2是针对某一类软件设计的一个具有讽刺意味的词语,在这种软件设计中,你实际上看不到任何人真正为软件设计做点什么。换句话说,整个软件结构就是一大团乱麻。
我们已经解释了大泥球会在Cucumber测试中引发哪些问题——缓慢的特性、固件数据和共享的环境,这些都是它可能导致的麻烦。要警惕这些信号并敢于对系统设计做出改变,从而使系统测试起来更加容易。
我们建议你把Alistair Cockburn的端口和适配器(ports and adapter)3架构作为设计系统的方法,从而使系统可测试。Michael Feathers的Working Effectively with Legacy Code [Fea04]一书提供了很多实用的例子,教你如何把原先设计时没有考虑测试的大型系统分解开来。
跟团队定期召开讨论系统架构的会议:大家喜欢哪些方面,不喜欢哪些方面,又有哪些方面是大家愿意接受的。大家总是容易被雄心勃勃的想法弄得情绪高涨,可回到座位上不久那些想法就很快消失在稀薄的空气中,因此要确保讨论结束时尽量得出现实可行的步骤,从而沿着正确的方向推动大家的工作。
以上基本涵盖了你在团队中采用Cucumber时可能遭遇的最常见的问题。然而,理解这些问题是一回事,抽出时间来处理它们则完全是另一回事了。下一节我们就来讨论一种帮你找到合适时间的重要技巧。