Cucumber:行为驱动开发指南

978-7-115-31885-5
作者: 【英】Matt Wynne 【挪】Aslak Hellesy
译者: 许晓斌王江平
编辑: 杨海玲

图书目录:

详情

本书是Cucumber的权威指南,两位作者是Cucumber的创始人和最有经验的用户和贡献者,它会提供使用Cucumber所需的全部知识,讲述如何用Cucumber指导开发过程。第一部分提供Cucumber入门所需的全部知识,第二部分通过可以工作的例子巩固学到的知识,同时学习一些更高级的Cucumber技术,第三部分提供一些解决问题的方法,针对的都是最困难、最常见的问题。

图书摘要

Cucumber:行为驱动开发指南

[英]Matt Wynne [挪]Aslak Hellesoy 著

许晓斌 王江平 译

人民邮电出版社

北京

本书的两位作者是Matt Wynne和Aslak Hellesøy,前者是Cucumber最有经验的用户和贡献者之一,后者是Cucumber的创始人,因此本书是一本权威指南,它会提供使用Cucumber所需的全部知识,让你和你的团队自信地开启Cucumber之旅。尽管Cucumber诞生于Ruby社区,但你可以用它测试几乎所有系统,从简单的shell或Perl脚本,到使用PHP、Java或任何其他平台编写的Web应用。

书中将展示如何用一组清晰、可执行且团队中任何人都能读懂的规格说明来表达用户那些天马行空的想法。你将学会如何将这些示例提供给Cucumber,并让它指导你的开发过程。本书的第一部分会提供Cucumber入门所需的全部知识,引导你从Cucumber的核心特性起步,通过 Cucumber的 Gherkin DSL,使用自然语言来描述客户想要的系统行为,然后带你编写Ruby代码来解释这些自然语言描述的规格说明并据此来验证应用的行为。第二部分将通过一个可以工作的例子来巩固学到的知识,同时学习一些更高级的 Cucumber 技术,还将学习如何测试异步系统和使用数据库的系统。第三部分提供了一些解决问题的方法,针对的都是作者曾帮助其他团队解决过的最困难、最常见的问题。基于这些模式和技术,你将学习如何使用Capybara和Selenium测试大量使用Ajax的Web应用,测试REST Web服务、Ruby on Rails应用、命令行应用、遗留程序等。

Cucumber有助于在软件团队中的技术人员和非技术人员之间架起沟通的桥梁。本书的内容既适合开发人员和测试人员阅读,也适合软件团队中的非技术读者阅读。

——The Cucumber Book译者序

软件开发领域有一个关于图书的“奥斯卡”大奖,自1991年起每年颁发一次,用以表彰每一年对该行业产生重大影响的图书,它就是 Jolt Awards,2012年这一奖项颁发给了《实例化需求》一书。《实例化需求》是一本讲述如何构建正确产品的书,它提倡借助实例的力量来促进开发人员、测试人员、产品负责人以及客户之间的协作,并总结了一套非常清晰的关键过程模式。当然,要实现这套模式,我们就不得不面对许多非常现实的问题,例如实例该如何编写,怎样驱动实例让它们自动运行产品,再具体一点,如果我的产品是一组Web服务,我该如何编写代码运行一个业务场景以达到验证和沟通的目的?这正是工具发挥用武之地的地方,而Cucumber可能就是这方面最佳的工具。

一说到“工具”二字,可能你想把手里捧着的书放一边了,“工具而已嘛,没什么干货”。一直以来很多程序员对工具有一种误解,他们认为问题原理是更重要的,工具只是辅助手段,这方面最极端的例子就是,有些程序员喜欢用文本文档写段代码以证明,只要编程能力高了IDE那样的工具就并不是必需的。我无意挑战问题原理的重要性,对编程来说,掌握编程范式、语意和语法无疑是至关重要的,但我想说的是工具同样重要!C#的重构工具 ReSharper能把一次重构的耗时从几十分钟降到几秒钟且更加安全;Unix下的命令行工具grep、sed和awk能把数百行的文本分析Java代码降到数十行且运行更快;较之于Subersion,Git大大简化了分支及合并代码的代价,由此衍生出来的GitHub更是极大地促进了开源项目的协作。类似的例子不计其数,借助正确的工具,程序员的生产力能得到极大地提高。不仅如此,工具还能改进我们的思维模式并强化这种改进,例如,当你熟练使用了重构工具后,你脑子里的思维单元是“重命名”、“提取方法”、“内联变量”这样的东西,而不是原始的修改单词、修改/添加方法名、修改/添加方法参数、删除变量等语法级别的思维单元。

这样的现象是有理论依据的,斯坦福大学的生物学和神经学教授Robert Sapolsky就认为

注释:① http://en.wikipedia.org/wiki/Robert_Sapolsky

注释:② Aspiration Makes Us Human:http://www.scientificamerican.com/article.cfm?id=aspiration-makes-us-human。

人类的一个决定性的、至关重要的特征是:我们总是通过创造各种工具,来使自身变得更迅捷、更聪明、更长寿,努力地打破我们在自然进化中所面临的各种限制。

Cucumber(中译名:黄瓜)是一款卓越的BDD工具,它的前辈是早已在Ruby社区家喻户晓的RSpec,而RSpec是基于RBehave改写的,RBehave又是Java世界BDD框架JBehave的Ruby移植,因此Cucumber已经从它的前辈们身上汲取了大量的经验,再加上Ruby语言本身出众的灵活性,这让使用 Cucumber 编写实例及代码成为了一种享受!有时候觉得这就像是小时候放学口渴了来一根自家地里的黄瓜,清爽可口!

本书有两位作者,Matt Wynee是Cucumber团队的核心成员,Aslak Hellesøy更是Cucumber这个项目的创始人。全书读来系统性非常强,也有一定的深度。在这样一本实战型的书中,我经常能看到作者们对软件设计、敏捷方法等方面精彩的解释,深入时可以详细地讲述应对遗留代码的策略,浅出时能够耐心地介绍项目的目录结构,不在书名中加入“深入浅出”可真是浪费了。

我希望阅读本书能给你带来享受,当然更重要的是从阅读第2章起就打开电脑跟着作者的样例练习,学习一样全新的工具时,没有什么比练习更重要了。另外,从今天起多吃点黄瓜,可减肥、可抗肿瘤,贴脸上还能美容……

注释:① 黄瓜是一种健康食品:http://www.whfoods.com/genpage.php?tname=foodspice&dbid=42。

在本书翻译到一半多的时候,我遇到了比较严重的视力问题,幸运的是好友王江平(@steedhorse)及时出手相助,完成了余下的部分,在此表示最诚挚的感激!澳大利亚莫纳什大学的陈少青同学也曾协助过本书的翻译,一并感谢。

许晓斌(@juvenxu)

杭州,2013/5/28

行为驱动开发(behavior-driven development,BDD),自从我在2003年首次提出并讨论它之后,已经取得了长足的进步。那个时候,我只是在试着寻找更好的方法来解释带有启发性的 TDD 实践,通常是对一些紧张不安、疑虑重重或至少持保留态度的程序员。为什么要在编码之前先写测试?没道理嘛。而且,为什么是我们要写测试——不是有测试人员吗?

很少有什么东西能代表真正范型(paradigm)上的转换。多数时候“范型”这一术语是市场营销人员用来说服你改换牙膏品牌的。根据免费在线词典(Free Online Dictionary)的解释,范型是“团体共享的一组假设、概念、价值和实践,它们构成了团体看待现实的方式。”没错,范型的转变会打乱一个人的现实感!难怪人们会不舒服。

注释:① http://www.thefreedictionary.com/paradigm

TDD就是少有的这种真正的范型转换之一,因此,在你尝试引入它的时候很多人深表怀疑也就不足为奇了。而同样不足为奇的是,我们需要多次以不同的方式,从不同的角度,向不同的受众来明确地表达,然后方能找到真正有效的东西。起初我们从代码深处开始,因为那是程序员关心的地方。后来我们便能采取行动更加接近业务干系人(business stakeholder)并描述多层次的方法,也就是现代BDD(同时也是传统TDD,这有点讽刺,Kent Beck从一开始就将TDD描述为工作在多个抽象层次上的)。

Aslak Hellesøy几乎从最开始就参与到对这一转变的描述中来。作为BDD的早期采用者,同时也是TDD的热情倡导者,他把我为RSpec构建一套场景运行器的痛苦努力重写成了如今我们称之为 Cucumber 的工具。他在工具及其社区上都投入了大量的时间和精力,因此,得知他和Matt在写一本用Cucumber实施BDD的书,我一点也不奇怪。我喜欢他们同时面向开发者和测试者这一做法。如果工具不能将这两个世界更紧密地联结在一起,那它就不是一种好的工具。

得知Aslak的“同谋”将是Matt Wynne,我很开心。作为另一名热情且有经验的 TDD实践者和BDD实践者,Matt从第一天就跟Cucumber结下不解之缘。他是一位风趣又有魅力的演讲者和极好的教师,其作品中知识与智慧源源不断。事实上,我甚至提议为Matt Wynne定义一个全新的描述成功的形容词——马氏成功(Matt Wynee),其程序介于一般成功(Win)与卓越成功(Epic Win)之间。(噢,太酷了,你注意到了吗?那不仅仅是一般意义上的成功,那是一种“马氏成功”!)

注释:①英语中Matt Wynne、Win和Epic Win这三个词是押韵的,这段话实际上是Dan开的一个玩笑。

我希望你能同我一样喜爱这本书。记得在我审阅早期书稿的笔记中出现过好多“哦,太可爱了!”这样的语句,具体多少记不清了。那种感觉就像被两位博学而又随和的向导带进一个陌生却又似曾相识的世界。数不完的示例、描述和边框注释(Joe——你很快就能见到这个名字——问了一些我发现自己也想问的东西,不久就成了我的朋友。)在学习的道路上为你提供帮助,同时作者们也努力使情节足够快地发展,从而让你保持专注,对一本技术书籍来说,这永远都是一种挑战。

我说不出再过8年BDD会在哪里,但有Matt和Aslak这样的人分享他们的革新和洞见,眼下就是投入敏捷软件开发的一个激动人心的时刻。

——Dan North,DRW Trading的精益技术专家,BDD发起者

Cucumber是一种非常友好的工具。它希望成为团队的一部分,且不介意做一个吹毛求疵的讨厌鬼。每个小组都需要这么一个角色,来记住关于系统能处理什么和不能处理什么的各种细节。

更为出色的是 Cucumber 会无偿做那些重复检查,以确保系统的运行符合预期。它可以把测试人员解放出来去做有意思、有创意的东西,并且给程序员必要时对代码做大手术的勇气。业务干系人对Cucumber这种开放的态度十分赞赏。Cucumber可以用他们能够理解的术语分享开发团队所做的一切。

Cucumber是一种新兴工具,但人们已对它有了些许误会。那些早期就开始接触Cucumber的人已经本能地意识到,Cucumber不仅是一种测试工具,更是一种协作工具。通过本书,我们希望自己不仅能为你展示怎样使用Cucumber,还能教会你如何更有效地利用Cucumber。

本书的目标读者

Cucumber旨在帮助软件团队在技术成员和非技术成员之间搭建一座桥梁。我们已经考虑到了这两类读者。本书主要写给那些至少掌握了一定编程技能且对自动化感兴趣的技术类读者。然而,本书的一些章节,尤其是前面解释如何编写规格说明本身的那一部分,主要是写给非技术读者的。具体来讲是以下几章。

• 第 1章 为何使用Cucumber

• 第 3章 Gherkin基础

• 第 5章 富有表现力的场景

• 第 6章 Cucumber常见问题及解决之道

• 第 13章 为遗留应用添加测试

随着内容的深入,我们将关注更加复杂的测试环境,并且读懂章节内容所需的技术层次也将升高。我们已经努力使知识结构尽可能地循序渐进,以便让刚刚接触测试和自动化的读者能跟上学习的步伐。

阅读本书不需要了解Ruby,但了解是有帮助的

Ruby是一种开源编程语言,可以在绝大多数操作系统上安装和运行。Cucumber最早的版本就是用Ruby写的,到今天它也是最流行的版本,本书正是关于这个版本的。

这并不是说被测系统必须用Ruby来写。Ruby的诸多优点之一是与其他语言和平台的完美交互。我们会向你展示如何利用Ruby工具来测试可用任何语言编写的基于Web的系统。

了解一点儿Ruby有助于跟上技术章节中的代码实例。Ruby语言学起来很容易,同时我们用到的Ruby实例也很简单。为了最大限度地吸取本书的精华,我们建议Ruby新手同时使用Everyday scripting with Ruby[Mar07]或者Programming Ruby:The Pragmatic Programmer's Guide[TFH08] 。

学习Cucumber不必基于测试驱动

从一个失败的Cucumber测试开始,然后通过这种失败来驱动应用代码的开发工作,作为一种由外向内的开发方法的一部分,我们已经用Cucumber取得了极大的成功。作为开发人员,这种工作方式能让我们实事求是,一步一个脚印,避免我们想当然地开发将来也许有需求但当前没有需求的功能。

Cucumber可以完善我们的工作方式,但它并不强制。一些团队利用Cucumber自动测试开发人员已经完成的工作。这是采用由外向内方法的第一步,因为Cucumber可读的测试已经吸引了团队中非技术干系人的视线并逐步使他们参与进来。即使用Cucumber编写针对已有代码的测试,你从Cucumber中的获得的好处仍然远远超出同类软件,比如QTP和Selenium IDE。我们相信本书会使你收获很多。我们并不是鼓吹这个过程,而是想和你分享关于哪些方法对我们行之有效以及为什么有效的一些感悟。

为何要听从我们

我们已经开发软件二十多年,运用自动测试也有十余年。Aslak在2008年开发了Cucumber。Matt从第一天就是最活跃的用户。

我们已经使用Cucumber测试过从Ruby on Rails Web应用到动漫游戏,再到企业Java Web服务在内的所有系统。我们还培训过成百上千的开发人员,教他们如何使用Cucumber,并在各种会议及世界各大公司中讲授书中的内容。

Cucumber 社区充满了有活力的争论。我们花费了大量时间与用户讨论,让大家挑战并打磨我们的想法。我们希望这本书已经涵盖了我们提炼出的尽可能多的知识和经验。

本书的组织结构

本书分为三个部分。第一部分我们主要带你了解使用Cucumber时需要明白的核心概念。初级读者会学到他们继续走下去所需了解的全部知识。已经体验过 Cucumber 的读者也可以了解到更多有用的细节。

第二部分贯穿了一个运用 Cucumber 开发新应用程序的示例。因为我们从设计应用程序起步,你将跟我们一起从零开始构造一个简单的应用,从而有机会体验我们所喜欢的利用Cucumber 开发软件的方式,并进一步巩固你在第一部分学到的知识。我们还会教你一些Cucumber的高级特性,这些特性结合示例讲解更易于学习。

Cucumber提供了一种定义并执行测试的框架,但需要测试的系统是多种多样的。在第三部分,我们为你提供了将Cucumber用于常见情形的广泛指导,比如用于REST API、Ajax Web应用和命令行应用的测试。

本书没有的内容

虽然用 Cucumber 测试动画和手机应用是可行的,但具体细节超出了本版的范围。在JVM、JavaScript 和 C#上运行的 Cucumber 实现允许你使用编写产品代码的语言来编写Cucumber代码,但这本书同样不会涵盖这一主题。Cucumber的连线协议(wire protocol,一种通过TCP套接字驱动远程系统的协议)也超出了本书的范围。

关于这一主题,我们在附录A中提供了更多信息的链接列表。关于运用Ruby自动化并测试不同系统的更多信息,作为对本书的补充,我们推荐你阅读 Ian Dees写的Scripted GUI Testing with Ruby [Dee08]。

运行代码示例

本书以示例为主,我们鼓励你跟着示例学习本书的绝大部分内容。边读边写能使你学到最多知识。但如果你喜欢,可以在下面的网址下载代码实例:http://pragprog.com/titles/hwcuc/source_code。

Windows用户

绝大多数代码示例在 Windows 和*nix 操作系统上运行时完全一样。极个别*nix 跟Windows系统不一样的情况,你会在附近的注释框中找到Windows版本,同时文本中会有说明告诉你去哪个注释框找。

很快你会注意到,我们使用$符号作为命令行提示符。这是大多数Linux和Mac用户所熟悉的,但Windows用户会感到些许陌生。所以看到类似下面的情况时:

$ cucumber

你可以视同看到的是:

C:\> cucumber

除此之外,其他都是一样的。

获得帮助

如果你在本书的任何一个环节上卡住了,可以上论坛 http://forums.pragprog.com/forums/166寻求帮助。

如果你有针对Cucumber的普遍性问题,Cucumber社区欢迎你向邮件列表https://groups. google.com/forum/#!forum/cukes发信。Cucumber是一种开源工具,意味着这个群体中的成员都是无偿奉献自己的时间的。所以,在发信寻求帮助时,务必确保自己已经彻底调查过相关问题。人们只有看到你尝试帮助自己时,才会更愿意帮助你。

软件始于一个想法。

我们假设这是一个优秀的想法—一个能让世界变得更加美好,或者至少能让一些人赚到一些钱的想法。而软件开发人员所面临的挑战就是要落实这个想法,使其能真正产生效益。

最初的想法是完美、漂亮的。如果拥有该想法的人碰巧是一个天才软件开发人员,那事情就非常简单了:他无须向任何人解释就能直接把想法实现成可工作的软件。然而更常见的情况是,拥有最初想法的人并不具备使其想法变为现实所必需的编程技能,因此这个想法必须从他的脑中传递到另外一些人的脑中。也就是说,相关的人需要沟通想法。

大部分软件项目会涉及多人紧密协作的团队,高质量的沟通对项目的成功至关重要。你可能已经知道,高质量的沟通并不仅仅是口若悬河地把你的想法描述给他人,你还需要收集可靠的反馈以确保对方正确理解了你的意思。这就是敏捷软件团队要用小型增量的方式开发软件的原因,采用增量方式开发出的软件可用来收集反馈,询问利益相关人:“是这个意思吗?”

但这仍然不够。如果开发人员误解了一个想法,然后花了两周的一次迭代实现了它,那么他不仅浪费了两周的时间,还因为引入了没有正确反映最初想法的概念和功能而破坏了代码的完整性。其他开发人员可能已经开始在这些错误想法的基础上不经意添加了更多代码,使它们基本上不可能完全从代码中消失。

我们需要一种过滤器,让我们的代码远离这些被误解的想法。

自动化验收测试的想法源自极限编程(eXtreme Programming,XP),具体说就是源自测试驱动开发(Test-Driven Development,TDD)实践。

注释:①《解析极限编程——拥抱变化》(Extreme Programming Explained: Embrace Change [Bec00])

注释:①《测试驱动开发》(Test Driven Development: By Example [Bec02])

不是让业务人员直接将需求交给开发团队,然后也没什么反馈的机会,而是让开发团队和业务人员合作编写自动化测试来表述业务人员想要的结果。我们称之为验收测试,因为这类测试表述了软件需要做什么才能让业务人员觉得软件可以被验收。验收测试写好的时候,运行它们肯定会得到失败的结果,因为实现代码还不存在,但验收测试抓住了业务人员真正关心的东西,并且让所有人明确了完成软件需要做哪些事情。

验收测试和单元测试不同,单元测试针对的是开发人员,帮助他们启动并验证软件设计。有一种说法是,单元测试确保你正确地编写软件,而验收测试则确保你编写正确的软件。

自动化验收测试作为一项既定实践已在很多 XP 团队中沿用多年,然而,许多经验不那么丰富的敏捷团队似乎把TDD看作仅仅针对程序员的实践。正如Lisa Crispin和Janet Gregory在《敏捷软件测试:测试人员与敏捷团队的实践指南》一书[CG08]中指出的那样,如果没有面向业务的自动化验收测试,程序员很难知道他们需要编写怎样的单元测试。自动化验收测试可以帮助团队关注重点,确保每次迭代你所做的都是你所能做的最有价值的工作。当然你仍然会犯错——但犯错的概率大大降低了,这意味着你可以准点到家,享受业余生活。

行为驱动开发(Behaviour-Driven Development,BDD)建立于测试驱动开发的基础之上,它标准化了那些优秀TDD实践者的良好习惯。优秀的TDD实践者以自外向内的方式开发软件,最初他们会编写一个失败的客户验收测试,该测试从客户的视角描述系统的行为。作为 BDD 实践者,我们细心编写验收测试,作为所有团队成员都能读懂的实例。我们使用这个编写实例的过程来获取业务人员的反馈,以便在开始实现软件之前,我们就知道自己是否是在编写正确的软件。在此过程中,我们会主动开发一种共享的通用语言(ubiquitous language)来描述和讨论我们开发的系统。

注释:② http://behaviour-driven.org/

正如Eric Evans在他的《领域驱动设计》[Eva03]一书中所描述的,很多软件项目都受过团队中领域专家和开发人员之间低质量沟通之苦:

“如果一个项目中的语言是支离破碎的,那么这个项目就面临着严重的问题。领域专家使用他们的行话,技术团队成员则拥有自己的、专门从设计角度讨论领域的语言……由于这种语言方面的分歧,领域专家描述他们的需求的时候非常模糊。开发人员努力尝试理解一个全新的领域,却只能得到模糊的结果。”

通过整个团队的自觉努力,项目涉及的所有人都能理解和使用的一款通用语言就会出现。如果整个团队在所有交谈、文档及代码中一致地使用这种语言,就不需要再花精力去翻译各自的不同方言,理解错误的概率也因此大大地降低了。

Cucumber为存在语言分歧的双方提供了可以会合的场所,从而促进了通用语言的发现和使用。Cucumber测试直接与开发人员的代码交互,同时它们又是用一种利益相关人能够理解的中间语言编写的。通过一起编写测试的方式,即协作描述(specifying collaboratively),团队成员不仅确定了下一步要实现的行为,也学会了怎样用一种大家都能理解的通用语言描述这种行为。

如果我们在开发开始之前就编写验收测试,就可以在错误的理解渗入代码之前发现并消灭它们。

Cucumber之所以能从大量的测试工具中脱颖而出,是因为它在设计时有一点非常明确,就是确保团队中的任何人都能够很容易地阅读或编写验收测试。这符合验收测试的本质——作为一种交流和协作的工具。Cucumber测试的易读性把利益相关人吸引到协作过程中来,帮助大家探索并真正理解他们的需求。

下面是一个Cucumber验收测试的实例:

Feature: Sign up

  Sign up should be quick and friendly.

 Scenario: Successful sign up

   New users should get a confirmation email and be greeted

   personally by the site once signed in.

  Given I have chosen to sign up

  When I sign up with valid details

  Then I should receive a confirmation email

  And I should see a personalized greeting message

  Scenario: Duplicate email

   Where someone tries to create an account for an email address

   that already exists.

  Given I have chosen to sign up

  But I enter an email address that has already registered

  Then I should be told that the email is already registered

  And I should be offered the option to recover my password

注意测试是如何描述为特定场景下我们期望的系统行为的实例(example)的。类似的实例能够强有力地帮助人们在系统被构建之前就将其形象化,效果通常超出大家的预期。任何团队成员都能阅读这样的测试,然后说出该测试是否反映了他对系统行为的理解,这样的测试也许还会激发他们进一步的思考,从而发现其他你未曾考虑的场景。Gojko Adzic的《实例化需求》一书包含了很多这方面的案例研究,案例中涉及的团队发现了实例的优点并将其充分利用。

以这种风格编写的验收测试已经不仅仅是测试了,它们是可执行的规格说明(executable specification)。

Cucumber测试同传统的规格说明文档一样能被利益相关人阅读和编写,然而其独特的优点在于,你可以在任何时刻给他们一台计算机让测试执行,结果会告诉你测试有多准确。在实际情况中,这意味着你的文档不再是一种写完后就慢慢过期的东西,而成为一种能随时反映项目真实状态的活的东西。

对于很多团队来说,Cucumber测试变成了关于系统行为的权威事实来源。拥有唯一的信息源之后,团队就能省下大量原本用于同步需求文档、测试和代码的时间。Cucumber测试也有助于在团队成员之间建立信任,大家不再对事实有各自不同的理解。

在深入本书的核心内容之前,我们先简要介绍一个典型的 Cucumber 测试集,以帮助你了解Cucumber是如何工作的。

Cucumber是一个命令行工具。运行时它会从普通语言编写的称为特性(feature)的文本文件中读取你的规格说明,解析需要测试的场景(scenario),然后针对你的系统运行这些场景以达到测试的目的。每个场景由一系列步骤(step)组成,Cucumber会一步步执行这些步骤。为了让 Cucumber 能理解特性文件,这些文件必须遵循一些基本的语法规则,这套语法规则就叫Gherkin。

除了特性文件,你还要为Cucumber提供一组步骤定义(step definition),它们是匹配特性文件中每个步骤的 Ruby 代码,业务语言描述的步骤行为都由这些步骤定义执行。在一个成熟的测试集中,这些步骤定义自身可能只包含一两行 Ruby 代码,具体的工作都代理给支持代码(support code)库来完成,应用程序领域特定的支持代码库知道如何执行系统的常见任务。通常这会涉及使用一个自动化库(automation library),例如浏览器自动化库Capybara,来与待测系统进行交互。

整个层次结构从特性往下至自动化库,具体如图1-1所示。

图1-1 Cucumber测试栈

如果步骤定义中的Ruby代码执行无误,Cucumber就依次执行场景中的下一个步骤。如果场景的所有步骤执行都没有错误,那么Cucumber就将该场景标记为通过。但是,如果场景中任何一个步骤失败了,Cucumber就会将该场景标记为失败并转而执行下一个场景。运行场景的时候,Cucumber 打印出相应的结果,告诉你它在干什么,以及它没干什么。

简单来说 Cucumber就是这么一回事。但作为一个杰出的自动化验收测试框架,Cucumber还有很多其他优点:你可以使用四十多种语言编写规格说明,可以使用标签(tag)把场景组织和归类,可以轻松集成大批高质量的 Ruby自动化库,来驱动几乎任何种类的应用程序。随着阅读本书的其他部分,所有上述及更多的内容都将清晰地展现在你面前。

我们来回顾一下到目前为止讨论了哪些内容。

只有开发人员和利益相关人一起清晰地交流的时候,软件团队才能工作得最好。要做到这一点有一种非常好的方法,就是让开发人员和业务人员基于自动化验收测试,协作描述需要完成的工作。

当验收测试以实例的形式编写时,它就能够激发人们的想象力,帮助人们发现之前未曾虑及的其他场景。

当团队协作编写验收测试时,他们可以开发出专属于相应问题领域的通用语言。这能帮助他们避免误解。

Cucumber的设计就是要帮助利益相关人参与到编写验收测试的过程中去。

Cucumber中每个测试用例称为场景,多个场景组成特性。每个场景包含多个步骤。

在Cucumber测试集中,面向业务的部分存储在特性文件中,为了能够让Cucumber正确读取文件,这些内容必须基于一套名为Gherkin的语法规则编写。

往下一层,步骤定义把面向业务语言编写的步骤翻译成Ruby代码。

为了阐明这些概念,下一章我们会进一步深入,我们会以用 Cucumber 来驱动开发的方式构建一个非常简单的应用程序。

尝试一下

Cucumber有其自身的通用语言。你能列出从本章中学到的Cucumber领域术语,并解释每个术语的含义吗?

我猜你已经等不及要立刻试试自己的“新玩具”了,下面我们通过一个简单的例子来体会一下使用Cucumber工作是怎样的感觉。在此过程中你也许无法完全理解其中的某些部分,但不必担心,接下来的几章我们会回过头来补充相应的细节。

我们将以由外向内的方式构建一个简单的命令行应用程序,整个开发过程由Cucumber驱动。注意我们是如何小步前进的,每次修改之后我们都会回头运行一下 Cucumber。这种不厌其烦的节奏对于有效使用Cucumber非常重要,关于这一点,实际演示比口头解释更有说服力。

如果想一步步遵循本章的演示的步骤来操作(你会发现这么做其实非常有趣),你需要首先安装Cucumber。如果你还未安装Cucumber,请参考附录B给出的安装指令。

好,那我们开始吧。

我们的目标是编写一个能够执行计算的程序,有些人称之为计算器。

关于这个计算器的未来,我们有一个美好的愿景:一个能够在手机、桌面和浏览器上运行的云端服务,借助通用的数学操作符“团结”整个世界。不过等等,我们是务实的业务人员,因此这个程序的第一个版本应该尽可能简单。第一个版本将是一个用 Ruby 脚本实现的命令行程序。它接受计算输入,计算完成后在命令行显示结果。

例如,如果输入的内容是

2+2

那么输出应该是4。

类似地,如果输入的内容是

100/2

那么输出就应该是50。

我想你应该明白了。

Cucumber 测试都归类为特性(feature)。我们使用这个名字是希望用它描述用户运行程序时使用的一些特性。首先我们要创建一个目录,该目录存放程序文件以及我们即将编写的特性文件。

$ mkdir calculator

$ cd calculator

我们要让 Cucumber引导整个计算器程序的开发过程,于是我们首先在这个空目录下运行一下cucumber命令:

$ cucumber

You don't have a 'features' directory. Please create one to get started.

See http://cukes.info/ for more information.

由于我们没有声明任何命令行参数,Cucumber 会认为我们使用了默认的features文件夹存放测试。这没什么问题,只是目前我们还没有任何测试。我们遵循这一目录结构的约定来创建一个features目录:

$ mkdir features

然后再运行一下Cucumber:

$ cucumber

0 scenarios

0 steps

0m0.000s

每个 Cucumber测试称为一个场景(scenario),每个场景都包含一些步骤(step),这些步骤告诉Cucumber具体做什么。上述的输出说明Cucumber现在已经能够正确扫描features目录了,不过它还没找到可运行的场景。下面我们来创建一个场景。

我们的用户调查显示,67%的数学运算是加法,因此加法是我们首先要支持的运算。你可以打开自己最喜欢的编辑器,创建一个名为features/adding.feature的文本文件,在其中添加如下内容:

下载first_taste/01/features/adding.feature

Feature: Adding

Scenario: Add two numbers

 Given the input "2+2"

 When the calculator is run

 Then the output should be "4"

这个.feature文件包含了计算器程序的第一个场景。我们将上一节的一个实例翻译成了 Cucumber 场景,以后我们可以让计算机一遍又一遍地运行这个场景。或许你已经看到, Cucumber对这个文件的结构实际上是有一些要求的,结构就是这里的Feature、Scenario、Given、When、Then等关键字,其他的所有内容都是文档。虽然书中这些关键字被标粗了(可能你的编辑器也会将它们标亮),但该文件只是简单的文本文件。这个结构就叫做Gherkin。

保存文件内容,然后运行cucumber,你应该能看到比上次多了好些输出:

$ cucumber

Feature: Adding

  Scenario: Add two numbers # features/adding.feature:3

   Given the input "2+2" # features/adding.feature:4

   When the calculator is run # features/adding.feature:5

   Then the output should be "4" # features/adding.feature:6

1 scenario (1 undefined)

3 steps (3 undefined)

0m0.003s

You can implement step definitions for undefined steps with these snippets:

Given /^the input "([^"]*)"$/ do |arg1|

  pending # express the regexp above with the code you wish you had

end

When /^the calculator is run$/ do

  pending # express the regexp above with the code you wish you had

end

Then /^the output should be "([^"]*)"$/ do |arg1|

  pending # express the regexp above with the code you wish you had

end

If you want snippets in a different programming language,

just make sure a file with the appropriate file extension

exists where cucumber looks for step definitions.

哇,突然来了这么多输出!让我们看看究竟发生了什么。首先,我们能看到 Cucumber找到了我们的特性并尝试运行它,这一步非常明白,因为 Cucumber 已经把这个特性的内容照搬到了终端上。你可能还注意到了输出摘要中原来的 0 scenarios 已经变成了 1 scenario (undefined),这表示Cucumber已经读取了我们的特性中的场景但还不知道如何运行它。

其次,Cucumber打印了三段代码,它们是用Ruby编写的步骤定义(step definition)样例代码,用来告诉Cucumber如何将日常英语描述的步骤翻译成一系列运行在我们应用程序之上的动作。下一步我们要做的就是将这些代码片段放入Ruby文件,然后开始丰富这些代码。

Gherkin 特性是面向业务的,再往下一层是步骤定义,不过在探索这一层之前有必要快速看一看全局图,以防有人感到困惑。图2-1可以提醒我们各种元素是如何组织在一起的,我们从包含场景和步骤的特性开始,场景中的步骤会访问步骤定义,后者将 Gherkin 特性和我们构建的应用程序连接在一起。

图2-1 Cucumber测试集的主要层次

现在我们将实现一些步骤定义,那样场景就不再是未定义(undefined)的了。

先不要过多考虑之前 Cucumber 输出的代码片段是什么意思,我们先把这些代码复制并粘贴到一个Ruby文件中。和特性文件一样,Ruby期望在约定俗成的位置找到步骤定义:

$ mkdir features/step_definitions

现在在fetures/step_definitions目录下创建一个名为calculator_steps.rb的文件,只要这是一个Ruby文件,Cucumber并不介意你给这个文件起什么名字,但这里我们给这个文件起的名字其实不错。接着用编辑器打开该文件并粘贴下面的代码片段:

下载first_taste/02/features/step_definitions/calculator_steps.rb

Given /^the input "([^"]*)"$/ do |arg1|

  pending # express the regexp above with the code you wish you had

end

When /^the calculator is run$/ do

 pending # express the regexp above with the code you wish you had

end

Then /^the output should be "([^"]*)"$/ do |arg1|

  pending # express the regexp above with the code you wish you had

end

运行cucumber,它会告诉我们下一步做什么:

Feature: Adding

  Scenario: Add two numbers

   Given the input "2+2"

    TODO (Cucumber::Pending)

   ./features/step_definitions/calculator_steps.rb:2

   features/adding.feature:4

   When the calculator is run

   Then the output should be "4"

1 scenario (1 pending)

3 steps (2 skipped, 1 pending)

0m0.003s

场景已经从未定义(undefined)升级到了待定(pending)。这是个好消息,因为它说明 Cucumber 正在运行第一个步骤,不过在此过程中它撞上了我们复制并粘贴的那些步骤定义代码中的pending标记,pending的意思是告诉Cucumber这个场景还是一个正在进行中的工作。我们需要用真正的实现替换掉这个pending标记。

注意,Cucumber 报告它跳过了另外两个步骤,只要遇到了失败的或者待定的步骤, Cucumber就会停止运行当前场景并跳过该场景剩余的步骤。

下面我们来实现第一个步骤定义。

我们已经确定计算器的第一个版本将会以命令行参数的形式接受用户输入,因此,对于Given the input “2+2”的步骤定义,我们的工作就是将输入记下来,以便下一步运行计算器的时候知道传入怎样的命令行参数。在features/step_definitions文件夹中,编辑文件calculator_steps.rb,修改第一个步骤定义如下:

下载first_taste/03/features/step_definitions/calculator_steps.rb

Given /^the input "([^"]*)"$/ do |input|

  @input = input

end

这里我们所做的是将特性的输入保存在一个 Ruby实例变量中,只要这个特定的场景还在运行,该实例变量就一直存在,因此我们可以在下一个步骤真正运行计算器的时候再次使用它。

不错,上一步很简单,现在我们该做什么呢?让我们问问cucumber:

Feature: Adding

  Scenario: Add two numbers

   Given the input "2+2"

   When the calculator is run

    TODO (Cucumber::Pending)

   ./features/step_definitions/calculator_steps.rb:9

   features/adding.feature:5

   Then the output should be "4"

1 scenario (1 pending)

3 steps (1 skipped, 1 pending, 1 passed)

0m0.003s

耶!我们的第一个步骤通过了!当然,整个场景还是处于待定状态,因为我们还有另外两个步骤需要实现,不过我们已经开始有了一些进展。

接下来我们实现下一个步骤,编辑features/step_definitions/calculator_steps.rb,修改第二个步骤定义如下:

下载first_taste/04/features/step_definitions/calculator_steps.rb

When /^the calculator is run$/ do

 @output = `ruby calc.rb #{@input}`

 raise('Command failed!') unless $?.success?

end

这段代码试图运行我们的计算器程序calc.rb,同时传入上一个步骤保存下来的输入,并用另一个实例变量保存输出。接下来它检查一个名字很特别的(实际上是很隐晦的)Ruby变量$?,来核实命令是否成功运行了,如果运行失败则抛出一个错误。记住,如果步骤定义抛出一个错误,Cucumber就会将步骤标记为失败,这就是实现这个目的最简单的方法。

这时我们运行Cucumber,应该就能看到它真正去尝试运行计算器了:

Feature: Adding

  Scenario: Add two numbers

   Given the input "2+2"

ruby: No such file or directory -- calc.rb (LoadError)

  When the calculator is run

   Command failed! (RuntimeError)

  ./features/step_definitions/calculator_steps.rb:10

  features/adding.feature:5

  Then the output should be "4"

Failing Scenarios:

cucumber features/adding.feature:3

1 scenario (1 failed)

3 steps (1 failed, 1 skipped, 1 passed)

0m0.026s

这次步骤失败了,因为我们还没有可运行的calc.rb程序。你应当看到Cucumber在步骤下面将抛出错误所产生的输出都用红色标亮了,这能帮助你定位问题。

你可能会想,明知 calc.rb 这个文件还不存在,却依然编写代码试图去运行该程序,这样做有点奇怪。我们是故意这么做的,因为我们想确保在深入到解决方法的实现之前拥有一个功能完备的测试。基于这条纪律编写测试意味着我们可以信任测试,因为我们看到这些测试失败了,当测试通过时我们就可以满怀信心地认为任务真的完成了。对于我们所谓的由外向内开发,这种优雅的节奏是其中的重要部分,初看起来这好像有点奇怪,我们希望通过本书展现这种开发方式的一些显著的益处。

由外向内开发还有另外一个好处:在未花任何精力实现计算器之前,我们就有机会从用户角度考虑计算器的命令行接口。而在这个阶段,如果我们意识到接口的一些问题,是非常容易做出改变的。

每次运行 Cucumber 的时候都查看输出中特性的全部内容很容易让人分散注意力,让我们切换到progress格式器(formatter)以获得重点更为突出的输出,运行如下命令:

$ cucumber –format progress

你可以看到如下输出:

.ruby: No such file or directory -- calc.rb (LoadError)

F-

(::) failed steps (::)

Command failed! (RuntimeError)

./features/step_definitions/calculator_steps.rb:10:in `/^the calculator is run$/'

features/adding.feature:5:in `When the calculator is run'

Failing Scenarios:

cucumber features/adding.feature:3 # Scenario: Add two numbers

1 scenario (1 failed)

3 steps (1 failed, 1 skipped, 1 passed)

0m0.083s

格式器

Cucumber格式器允许通过不同方式来让测试的输出可视化。有生成HTML报告的格式器,有针对 Jenkins之类的持续集成服务器生成 JUnit XML的格式器,还有很多其他种类的格式器。

你可以使用 cucumber –help查看可以使用的各种格式器,也可以自己试试这些格式器的效果。我们将在第11章进一步解释格式器。

不再打印整个特性,取而代之的是progress格式器在输出中打印了三个字符,每个步骤对应一个字符。第一个字符 .表示该步骤通过了;第二个字符F表示该步骤失败了,如我们所知;最后一个字符 –表示最后一个步骤被跳过了。Cucumber拥有多种不同的格式器,你可以在运行特性的时候用它们生成不同类型的输出,在阅读本书的过程中你还会学到更多格式器的用法。

以上内容是小小的调剂,现在我们回到工作中来,我们还有一个失败的测试要修复!

继续遵照Cucumber的指示,我们需要为计算器程序创建一个Ruby文件。让我们暂时先创建一个空的Ruby文件,这样在转入解决方案之前,我们可以继续停留在外部并完成测试。Linux/Mac用户可以用如下命令创建空文件:

$ touch calc.rb

如果用的是Windows,就无法使用touch命令了,可以用编辑器创建一个名为calc.rb的空文本文件,或者使用如下技巧:

C:\> echo .> calc.rb

当我们再次运行cucumber的时候,就可以看到第二个步骤通过了,并且运行到了最后一个步骤:

$ cucumber –format progress

..P

(::) pending steps (::)

features/adding.feature:6:in `Then the output should be "4"'

1 scenario (1 pending)

3 steps (1 pending, 2 passed)

0m0.021s

要让最后一个步骤定义生效,将features/step_definitions_calculator_ steps.

rb中最后的步骤定义修改如下:

下载first_taste/07/features/step_definitions/calculator_steps.rb

Then /^the output should be "([^"]*)"$/ do |expected_output|

  @output.should == expected_output

end

我们使用RSpec断言来检查特性中指定的期望输出与前一个步骤定义中用@output存储的程序输出是否匹配。如果不匹配,RSpec 会产生一个错误,就像我们在上一个步骤定义中使用raise一样。

注释:① The RSpec Book [CADH09]

现在再次运行cucumber,我们就会得到一个真正失败的测试:

$ cucumber --format progress

..F

(::) failed steps (::)

expected: "4"

  got: "" (using ==) (RSpec::Expectations::ExpectationNotMetError)

./features/step_definitions/calculator_steps.rb:16:in `/^the output should be

"([^"]*)"$/'

features/adding.feature:6:in `Then the output should be "4"'

Failing Scenarios:

cucumber features/adding.feature:3 # Scenario: Add two numbers

1 scenario (1 failed)

3 steps (1 failed, 2 passed)

0m0.021s

非常好!现在我们的测试失败得合情合理了:它运行我们的程序,检查输出,然后告诉我们正确的输出应该是怎样的。到现在该暂停工作,休息一下了。我们已经为这个版本完成了不少艰苦的工作:一会儿等我们回来看代码的时候,Cucumber会准确地告诉我们要使程序正确运行还需要做哪些工作。要是我们所有的需求都像这样伴随着一个失败的测试交到我们面前,构建软件将会变得多么简单!

尝试一下

你能写一个让场景通过的calc.rb实现吗?记住,在目前阶段,我们只需满足一个单独的场景,因此,一个非常简单的方案就能解决该问题。

下一节我们将会展示我们的解决方案。

既然已经有了可靠的失败场景,那就是时候让这个 Cucumber 场景指导我们编写解决方案了。

有一个非常简单的方案能让测试通过,但该方案其实不会有实际的帮助,不管怎样我们先试一下,哪怕为了好玩儿:

下载first_taste/08/calc.rb

print "4"

试试运行cucumber,你会看到场景最终通过了:

...

1 scenario (1 passed)

3 steps (3 passed)

0m0.025s

很好!不过这个方案有什么问题呢?毕竟我们说过希望做能让测试通过的最少的工作,对不对?

事实上,这与我们之前说的不完全一样,我们说希望做能让测试通过的最少的、有用的工作。这里我们做的确实让测试通过了,但并不十分有用,暂且不说它根本没有计算器的功能这一事实,我们先看看这行耍小聪明的代码在测试时漏掉了什么。

• 我们没有尝试从命令行读取输入。

• 我们没有尝试做加法运算。

在《Crystal Clear:小团队的敏捷开发方法》[Coc04]一书中,Alistair Cockburn提倡在项目中尽早构建一个可行走的骨架(walking skeleton),以便发现技术选型的任何潜在问题。显然我们的计算器非常简单,但一样值得我们考虑一下这条原则:为什么我们不构建一种能通过该场景的更有用的东西,并且让它帮助我们更多地了解自己打算使用的实现呢?

如果你不能信服这种观点,可以尝试将这种解决方案看成一个代码重复的问题。我们在两个地方硬编码了4这个值:一处是场景中,另一处是计算器程序中。在更复杂的系统中,类似的重复可能不会被注意到,因而使场景变得脆弱。

让我们强迫自己修复这个问题,可以使用Kent Beck在《测试驱动开发》一书中所说的三角法(triangulation)。我们使用一个新的名为Scenario Outline(场景轮廓)的关键字来为特性添加另一个场景:

下载first_taste/09/features/adding.feature

Feature: Adding

Joe问:

我觉得很怪异,你一直在让测试通过但毫无作用!

我们实现了一个步骤,它调用了计算器程序然后就通过了,即便这个时候“计算器”还只是一个空文件。这到底是怎么回事?

记住一个步骤本身并不是一个测试,测试是整个场景,除非所有步骤都通过了,否则场景不可能通过。在我们实现所有步骤之后,只有一种办法能让整个场景通过,那就是构建一个能运行加法运算的计算器!

像这样由外向内工作的时候,我们常常会使用空计算器程序这样的桩(stub),把它作为一个将来需要填充的预留区域。我们知道不可能永远把空文件留在那里并侥幸成功,因为最终 Cucumber 会告诉我们,要让整个场景通过就必须回来给空文件夹添加内容,使它能做点有用的事。

故意只做能让测试通过的最少的、有用的工作,这一原则看起来很懒,但实际上是一条纪律,它能保证我们的测试彻底且周密:如果测试没有驱动我们编写正确的软件,那么我们就需要更好的测试。

Scenario Outline: Add two numbers

 Given the input "<input>"

 When the calculator is run

 Then the output should be "<output>"

 Examples:

   | input | output |

   | 2+2 | 4|

   | 98+1 | 99|

我们把场景转变成了场景轮廓,这使我们可以用表格指定多个场景。另一种方法是复制并粘贴原来的整个场景然后改变其中的一些值,但我们认为使用场景轮廓在表述实例方面可读性更强,并且我们想让你体验一下 Gherkin 语法允许的其他写法。我们看一下现在输出是什么样子:

$ cucumber

Feature: Adding

  Scenario Outline: Add two numbers

   Given the input "<input>"

   When the calculator is run

   Then the output should be "<output>"

  Examples:

    | input | output |

    | 2+2 | 4|

    | 98+1 | 99|

    expected: "99"

     got: "4" (using ==) (RSpec::Expectations::ExpectationNotMetError)

   ./features/step_definitions/calculator_steps.rb:15

   features/adding.feature:6

   Failing Scenarios:

   cucumber features/adding.feature:3 # Scenario: Add two numbers

   2 scenarios (1 failed, 1 passed)

   6 steps (1 failed, 5 passed)

  0m0.072s

从摘要中的 2 scenarios (1 failed, 1 passed),我们可以看到Cucumber已经运行了两个场景,Cucumber运行场景轮廓的时候,它会把实例(Examples)表中的每一行扩展成一个场景。第一个实例(结果为4的那个)仍然通过了,但是第二个实例失败了。

现在,肯定应该用一个更切实的解决方案重新实现我们的程序了:

下载first_taste/10/calc.rb

print eval(ARGV[0])

首先我们读取命令行参数ARGV[0],然后把它传给Ruby的eval方法。这足以算出与计算器输入相关的结果,最后我们将结果打印到终端。

试一下,是不是两个场景都通过了?很好!你已经构建了自己的第一个用 Cucumber 驱动的应用程序。

在本章中我们快速浏览了许多不同的内容,所有这些内容都会在后面再次详细介绍,现在来做一个简单的小结并强调几个最重要的地方。

Cucumber希望你用约定的目录结构来存储特性和步骤定义:

features/

 adding.feature

 …

 step_definitions/

  calculator_steps.rb

  …

如果你真的需要,也可以传递参数给Cucumber,从而改变默认结构,优先使用自己指定的目录结构,但这种按约定存储文件的方法是最简单的。

在我们慢慢推进这个例子的进程中,你是否注意到我们多久运行一次cucumber?

关于使用Cucumber由外向内开发这种方式,有一点我们非常喜爱,就是它能帮助我们保持注意力集中,我们可以让 Cucumber指导自己一步步向前,直到完成工作,让我们把精力集中在如何创建一个优雅的解决方案上。每做一处修改都运行一次Cucumber,就能快速发现引入的错误并立刻修复,有关整个工作的进展,我们也得到了大量的反馈和激励。

Cucumber测试通过名为Gherkin的语法描述,Gherkin文件是使用.feature扩展名的简单文本文件。我们将在第3章中进一步介绍Gherkin。

步骤定义是 Cucumber 测试和待测应用程序之间的 Ruby“粘合剂”,当所有元素一起工作时,整个情形如图2-1所示。

你会在第4章中学到更多关于步骤定义的内容。

经过这个短暂的Cucumber特性旅程之后,我们将放慢速度并逐渐深入。我们将在接下来的几章中一层一层地介绍每个主题,首先从用来编写Cucumber特性的语言——Gherkin开始。

尝试一下

看看自己是否能用本章开头的另外一个实例来添加第二个特性,即division.feature,你需要修改解决方案以使该特性通过吗?

如果团队是第一次用 Cucumber,用不了多久你就会注意到自己写的代码 bug 比以前少了。你发现自己可以勇敢地重构那些以前碰都不敢碰的代码。看到自己的第一个场景通过时的那种喜悦,鼓舞着你不断添加一项又一项特性。

然而,一段时间后,事情开始变味了。突然间你发现测试运行的时间实在太长;或者你开始注意到有几个场景会随机地失败,而且通常是在紧张的工期已经临近的时候;也可能不懂技术的利益相关人对这种开发过程兴趣渐失,只剩下开发人员还在阅读那些特性。人们甚至开始问这样的问题:

Cucumber是不是妨碍了我们的工作?

好消息是,这些问题都是有办法解决的。我们在教练和顾问咨询工作中曾见过各种各样的团队在学习使用 Cucumber 时遭遇各式各样的问题。在本章中,我们将描述曾经见过的最常见的问题。我们会帮你理解这些问题的根源,给出应对它们的建议,或者,更理想一点儿,从一开始就避免它们。本章没有太多代码,但有大量有用的建议。

我们首先从问题入手,描述你的团队可能经历的4种不同症状,然后我们会深入分析这些问题背后的原因,最后考察解决的方法。到本章结束时,你应该会对如何帮助团队从长远角度成功运用Cucumber持有更大的信心。

我们从Cucumber出现问题时团队可能感受到的痛苦中找出了主要的4种类型。看看其中有没有你熟悉的。

下面我们仔细看看上表中所示的每一种症状。

一个场景,同样的源代码同样的环境,昨天还能通过,今天却失败了,我们将这种情形称为闪烁的场景。下面是我们对闪烁的场景的定义。

闪烁的场景

闪烁的场景偶尔失败,随机失败。同一个场景在相同环境的同一套代码库上运行,大多数时候能通过,有时却失败。这些似乎难以控制的失败使团队对测试、代码和自身都失去了信心。

闪烁的场景最让人讨厌的一点是:一旦你尝试重现它以便修复时,它反而不再失败了。修复闪烁的场景是最困难的任务之一,也是最重要的任务之一。一组自动测试套件要想有用,团队必须完全信任它。如果连单个测试都在破坏这种信任,那就会腐蚀团队中各个成员对整个测试套件的信任感。

为修复闪烁的场景,你必须研究代码,努力搞清楚它为什么会发生。这是一种科学过程:对失败原因做出一种假设,设计一个实验来证实或证伪这一假设,然后执行实验看自己是否正确。你可能需要多次重复这一循环才能找到问题的答案,如果闪烁的场景总是间歇地失败,执行一个实验可能需要好几天。如果想法儿用光了,宁可考虑将整个测试删除,也不要由着它自行选择失败的时间再回来折腾你。

闪烁的场景通常由以下问题之一所引发。

• 共享的环境,参见 6.3.3节。

• 渗露的场景,参见 6.3.1节。

• 竞争条件和打瞌睡的步骤,参见 6.3.2节。

你感觉测试套件让你几乎无法写代码,因为总会有明显不相关的测试亳无理由地失败,我们将这种情况称为脆弱的特性。下面是我们对脆弱的特性的定义。

脆弱的特性

脆弱的特性极易被破坏。特性脆弱时,在测试套件或主代码库的某个部分做些必要的修改会破坏明显不相关的场景。

遇到脆弱的场景时,通常你是在做其他事情的时候。你被不期而至的失败中断了,只好赶紧花时间去修复这意料之外的测试失败。运气不好的日子里,这种情况会多次发生,害你深陷其中而迟迟不能自拔。脆弱的特性具有自我实现性:当开发人员察觉到自己的测试脆弱不堪时,他们常常就更没有勇气重构或清理测试代码,相反他们会尽量快进快出,以求早一点远离是非之地,于是测试和产品代码便越来越难维护了。

脆弱的特性通常由以下问题之一所引发。

• 固件数据,参见 6.3.5节。

• 重复,参见 6.2.3节。

• 渗露的场景,参见 6.3.1节。

• 被隔离的测试人员,参见 6.3.4节。

每次往测试套件中添加一个新的场景,便在测试运行的总体时间中增加了几秒。对一个用户不断要求新特性的成功应用来说,测试运行的时间只会变得越来越长。长时间的测试正慢慢向你靠近:一开始5分钟已经够让人难耐了;之后,15分钟,虽然糟糕,但你已经习惯了在它运行时去喝杯咖啡;用不了多久,等你喝完咖啡回来它依然没有结束,15分钟变成了25分钟;然后在不知不觉中,你的特性已经需要运行一小时甚至更长时间了。

新的场景一旦通过,继续运行它的主要原因就是获得反馈:如果你不小心破坏了它所检查的功能,你希望场景可以给出警告。可随着测试运行的时间越来越长,这种反馈的价值也就减少了。项目构建太慢时,开发人员在提交代码前便不再运行全部测试,转而依靠持续集成服务器来提供反馈。如果多名开发人员同时这么做,他们所做的全部修改能集成到一起的概率也就不敢指望了,失败的构建于是就成了家常便饭。

测试运行时间长还意味着人们不敢动手对 Cucumber 测试做重构或其他常规维护。如果某个步骤定义在340个场景中使用,重构其代码是骇人的,因为你需要运行全部340个场景才能确切地知道自己的改动有没有破坏了什么。

缓慢的特性通常由下列问题的某种组合引发。

• 竞争条件和打瞌睡的步骤,参见 6.3.2节。

• 大量场景,参见 6.3.6节。

• 大泥球,参见 6.3.7节。

有的团队尝试使用 Cucumber,最终却只把它当做了测试脚本的自动化工具,这样的团队中,最常听到的一句抱怨就是“利益相关人不愿阅读我们写的特性”。但也有许多团队证实Cucumber确实能带来改观,有助于开发团队更加有效地同业务利益相关人协作。这两种不同的体验差别源自哪里呢?

答案的一部分在于要从一开始就同业务利益相关人建立正确的协作关系。如果他们觉得自己太忙,没时间帮你准确理解他们想要的东西,那么你面对的是一个更深层的团队问题, Cucumber爱莫能助。但另一方面,许多团队开始时倒是有热心主动的利益相关人,可团队却浪费了Cucumber带来的建立这种协作关系的机会。如果测试人员或开发人员独自编写特性,他们就难免使用技术术语,这样利益相关人在阅读的时候会觉得自己被边缘化了。这会变成恶性循环:利益相关人本来可以帮助你使用对他们有意义的语言编写特性,但随着兴趣渐失,他们花在这上面的时间会越来越少。不知不觉中,特性就沦为纯粹的测试工具了。

这一痛苦症状通常由下列问题的某种组合引发。

• 偶然细节,参见 6.2.1节。

• 命令式步骤,参见 6.2.2节。

• 重复,参见 6.2.3节。

• 语言不通用,参见 6.2.4节。

• 闭门造车式的特性,参见 6.2.5节。

团队中一旦发现这类症状,就需要知道如何处理。这正是调查工作背后存在的问题并考虑应对措施的时机。

Cucumber的特性正是Gojko Adzic所说的活文档(living documentation)。这一术语恰如其分地总结了使用Cucumber的两大好处。

注释:①《实实化需求》(Specification by Example [Adz11])

• 活的:能自动测试系统,以便你可以安全地工作。

• 文档:便于大家有效地沟通系统的当前行为和预期行为。

并行运行Cucumber测试

如果你卡在了一组运行缓慢的特性上,一种实用的方法是让它们并行运行。最简单的并行方法是用标签或文件夹来把特性划分成不同的部分,然后让每一部分同时运行。许多持续集成工具,比如 Jenkinsa,支持将构建委托给从属机(slave machine),这可以确保每部分特性都能获得专属于自己的环境。

注释:a.http://jenkins-ci.org/

另一种方法是使用Testbotb或Hydrac这样的工具将特性自动分布到多台从属机上。不论选择哪种方法,你肯定都要用到本章后面讲到的“一键式系统搭建”,(参见6.3.5节)。

注释:b.http://rubygems.org/gems/testbot

注释:c.http://rubygems.org/gems/hydra

团队因为 Cucumber而痛苦挣扎的时候,背后存在的问题会使你在以上两个方面的某一个上栽跟头。这些问题要么会导致 Cucumber场景为开发人员带来劣质反馈,要么意味着Cucumber未能给团队的沟通带来帮助。我们将首先考察哪些因素会妨碍特性成为沟通工具。

考虑下面这个为在线邮件客户端编写的场景:

Scenario: Check inbox

 Given a User "Dave" with password "password"

 And a User "Sue" with password "secret"

 And an email to "Dave" from "Sue"

 When I sign in as "Dave" with password "password"

 Then I should see 1 email from "Sue" in my inbox

这个场景中有很多细节:有主要角色Dave 的用户名和密码,还有另一个用户Sue 的用户名和密码。用户名非常有用,因为它们有助于场景故事的描述,而密码就是噪音了:用户密码与被测内容毫无关系,事实上却让测试更难读懂。比如,Sue的密码跟Dave不同。阅读场景的时候,你会疑惑这是否重要,场景的主要目的是验证Dave 可以看Sue 的邮件,你的注意力却被分散到别处去了。

像密码这种在场景中提及但实际上与场景的目标毫无关系的细节,我们称之为偶然细节(incidental detail)。这种不相关的细节使得场景很难阅读,而这又会让利益相关人对阅读场景失去兴趣。我们去掉密码,把场景重写一下:

注释:①这一术语来自Dale Emery的杰出论文“编写可维护的验收测试”:http://dhemery.com/pdf/writing_maintainable_automated_acceptance_tests.pdf。

Scenario: Check inbox

 Given a User "Dave"

 And a User "Sue"

 And an email to "Dave" from "Sue"

 When I sign in as "Dave"

 Then I should see 1 email from "Sue" in my inbox

这绝对是一种改善,它使场景的实质性内容更易于阅读和理解了。我们进一步去掉更多噪音:

Scenario: Check inbox

 Given I have received an email from "Sue"

 When I sign in

 Then I should see 1 email from "Sue" in my inbox

现在我们有了一个简洁清晰的三步场景。可维护性也更好:如果产品负责人(product owner)想让我们修改身份验证机制,我们只需修改下层的步骤定义代码,而不必去动特性。

避免偶然细节

如果你是一名程序员,或许早已熟练了每天在阅读代码的时候滤掉不相关的细节。编写场景的时候更要时刻想着这一点,因为一不留神这些偶然细节就会溜进来。

编写场景的时候,尽力避免被已有的步骤定义所左右,只管用直白的英语把你希望发生的事情确切地写下来即可。事实上,尽量不要让程序员或测试人员独自编写场景。让非技术的利益相关人或者分析师从纯粹以业务为中心的角度出发编写每个场景的初稿,或者最理想的情况是与程序员结对,从而分享他们的构思模型。有了工程意义上设计精良的支持层,你便可以信心百倍地快速编写新的步骤定义来配合场景的表达方式。

要让计算机为你做事,你需要给它提供指令,在计算机程序设计中,关于如何表达这些指令有两种对比鲜明的风格,分别称为命令式编程(imperative programming)和声明式编程(declarative programming)。

命令式编程是指使用一个命令序列,让计算机按特定的次序执行它们。Ruby就是命令式语言的实子:你把程序写成一系列的语句,Ruby按顺序每次执行其中的一条语句。声明式编程则告诉计算机应该做什么(what),而并不精确指明如何(how)去做。CSS就是声明式语言的实子:你告诉计算机希望Web页面上的各种元素如何呈现,剩下的让计算机去处理。

Gherkin当然是命令式语言。Cucumber按照你编写的顺序依次执行场景中的每个步骤,每次执行一步。但这并不意味着这些指令读起来要像装配组合家具的说明书一样。我们先来看看用命令式风格编写场景步骤的典型实子:

Scenario: Redirect user to originally requested page after logging in

 Given a User "dave" exists with password "secret"

 And I am not logged in

 When I navigate to the home page

 Then I am redirected to the login form

 When I fill in "Username" with "dave"

 And I fill in "Password" with "secret"

 And I press "Login"

 Then I should be on the home page

这个场景有什么好呢?好吧,它使用了非常通用的步骤的定义,如/^I fill in "(.*)" with "(.*)"$/"之类,这意味着你可以编写大量与之类似的场景,而无须创建太多的步骤定义代码。你或许还可以说它可以指导用户界面的设计,因为它为登录表单中使用的字段和按钮都取好了名字。

然而,团队如果使用这样一种命令式风格来编写步骤定义,用不了多久他们就会遭受脆弱的测试以及厌倦的利益相关人等痛苦。以这种方式编写的场景不仅嘈杂、冗长,读起来令人厌烦,而且很容易遭到破坏:如果负责用户体验的同事决定将提交按钮的措辞由Login改为Log in,场景就会失败,几乎莫名其妙地失败。

最严重的是,使用这样的泛化步骤定义写出的场景无法创建出场景的领域语言。基于fill in 和 press 这样的词语,这一场景的语言所表达的领域是用户界面控件,属于泛化且层次较低的一个领域。

改用声明式风格

我们来把场景的抽象层次提高一下,通过更加声明式的风格来重写场景:

Scenario: Redirect user to originally requested page after logging in

 Given I am an unauthenticated User

 When I attempt to view some restricted content

 Then I am shown a login form

 When I authenticate with valid credentials

 Then I should be shown the restricted content

这种风格的漂亮之处在于它不与用户界面的任何特定实现相耦合。同样的场景可应用于胖客户端,也可应用于移动应用。它使用的不是技术词语,而是用一种任何对网络安全感兴趣的利益相关人都能明确理解的语言(unauthenticated、restricted、credentials)编写的。唯有从这样的抽象层次上表达每一个场景,团队的通用语言方得显现。

从命令式到声明式的风格频谱

Gherkin 特性中在命令式风格和声明式风格之间并没有明确的界线。相反,这是一个连续的频谱,每个场景中每个步骤在频谱上的正确位置取决于许多方面:你所描述的系统领域,你所构建的应用类型,程序员的领域知识,以及非技术利益相关人对程序员的信任水平。如果利益相关人希望在特性中看到许多细节,这或许表明你需要努力改善这一信任,但也可能说明你们开发的系统就是需要详述很多细节的。

声明式风格也可能被用得太过,从场景中去掉的细节太多,结果它连一个故事都讲不具体了:

Scenario: The whole system

 Given the system exists

 When I use it

 Then it should work, perfectly

这个场景当然是荒谬可笑的,但它说明了当你把抽象层次抬得太高,以至于场景不能告诉阅读者任何能引起兴趣的内容时结果会怎样。使用这一场景的团队需要对它们的程序员有不可思议的信任程度。我们鼓励你督促团队向频谱中更抽象、更声明式的一端努力,然而,最重要的永远是跟你们的利益相关人一起工作,从而找出最适合他们的抽象层次。

使用命令式风格的确意味着你必须编写更多的步骤定义,但你可以把实际工作推给支持代码中的辅助方法(helper method),从而使步骤定义的代码保持简短和易于维护。我们将在第8章向你展示这种做法。

所有好的计算机程序员都明白重复对于代码的可维护性有多大的害处,然而我们还是经常看到团队的 Cucumber 特性中充满了重复。重复显然会让你的场景脆弱不堪,此外也让场景读起来单调乏味。

我们在第5章中演示过,Gherkin提供了可用来减少重复的Background和Scenario Outline关键字,但有时重复是一个信号,说明编写步骤所用的抽象层次太低,要对这种情况保持警觉。最好跟团队中的非技术成员一道工作,获得他们的反馈:哪种重复他们可以接受,哪种重复会让他们目光呆滞。

让实例流动起来

作者Dan North

DRY原则(Don’t Repeat Yourself)指出,任何概念的定义应当在代码中出现且仅出现一次。这是一个了不起的目标,因为你如果必须对系统的行为做出修改,肯定希望只在一个地方修改,同时又对改动很有信心,相信它会一致地应用于整个代码库。如果那种行为散落在代码的多处定义中,不但你自己无法全部找到它们,很可能在你之前因为别人也不能全部找到,结果那些定义已经彼此不一致了,谁愿意这样呢?

注释:①《程序员修程之道:从小工到从家》(The Pragmatic Programmer: From Journeyman to Master [HT00])

然而,在你用实例驱动代码的时候,我相信有另外一个原则优于DRY:实例应当讲出一个好故事。实例是给未来的程序员(包括你自己,如果在3个月以后你会回来修改这段代码的话,那时你已经忘记它做的是什么了)提供指导的文档叙述。这种情况下,目标的清晰性将显现于记述的质量中,而未必体现在最少的重复中。

几年前,我第一次与Martin Fowler结对编程。我是说我已经有过不少结对编程的经历,只是从未与Martin结对过。我们一起看一些Ruby代码,是我用测试优先的方式实现的,Martin 要求通过看测试来“弄清代码做什么”。这时他做了一件十分古怪的事。他开始把测试四处移动。我在代码中写过几个辅助类和工具方法,它们干净地呆在源文件的底部。Martin把它们移到上面,直接放在使用它们的第一个测试之前。

疯了!我想。现在到处都是支持代码了!这真的冒犯了我的整洁感。但这时我看到有一种模式浮出水面:测试代码显得更像个故事了。他让这些小小的方法和类正好出现在故事讲述中对应的那一行情节之前。这着实让我开了眼界。测试代码流动了起来,展开了被测类的故事。

后来,我用一本书作为类比再想这件事,心里豁然开朗了。假设你读这样一本书:里面所有的情节和人物都按照 DRY 原则给抽干了,一切只出现在脚注或附录中。所有的人物描述、情节元素、潜台词等都被仔细抽取出来,做成交叉引用的章节。如果你是在读百科全书,这非常好,但如果你想投入到故事流中,弄清楚发生的事情,这样就不太合适了。你会在书中无休止地来回翻阅,而且很快会忘记故事看到哪儿了。借用一个古老的玩笑,字典里面密密麻麻全是情节,但在“情节”的行进中,它们至少解释了所有的单词。

有些人把这称为DAMP(Descriptive and Meaningful Phrases)原则:描述性且有意义的叙述。编写实例的时候,可读性压倒一切,于是DAMP高于DRY。

团队使用的通用语言将由系统涉及的领域来驱动。如果你们在构建面向现场音乐爱好者的系统,通用语言将包含音乐会、演出、表演者和场地之类的词语。如果你们在做电视节目预告,通用语言中将有播音员、节目类型、节目长度和播出日期之类的词语。

关键在于团队的所有成员在所有场合都使用同样的词语。如果一个数据库表名叫 tbl_Performer,而其中的数据行所表示的东西被团队中多数人称为artists,那是不可接受的。每当出现这样的术语分歧的时候,大家应该马上停下来,确定哪个才是应该使用的,适当纠正后就坚持使用它。

我们讨论的是开发一种通用语言,因为这是一个持续进行的过程。这一开发过程需要我们工作上的投入。真正做到彼此倾听并且就使用的词语达成一致是需要努力的,而坚持这种约定也是需要纪律的。

然而,回报是巨大的。使用通用语言的团队犯错更少,且更能享受工作的乐趣,因为他们能有效地沟通工作内容。不重视通用语言价值的团队将会粗枝大叶地使用场景中的语汇,从而丧失在团队中从注技术和从注业务的双方之间构筑坚固桥梁的宝贵机会。那时,如果你试图纠正别人或澄清术语,带给人的感觉只会是吹毛求疵。

花点时间向团队解释一下通用语言的概念及益处。一旦大家都理解它为什么重要,你会发现大家会更乐于花精力讨论并决定使用哪些词语更合适。

如果用得正确,Cucumber可以帮助团队把通用语言开发出来。在程序员和业务人员协同编写场景的时候,你会发现关于用词精确性的各种争论会时不时地暴发。非常好!每一次分歧都暴露了两班人马之间一处潜在的误解,或者说一个 bug。对新团队来说,这样的讨论开始会困难一些,但随着这一语言的不断开发,事情将变得越来越容易。下面的“三位朋友”提供了组织这种会议一种好方法。

人们会觉得 Cucumber 是一种技术性较强的工具。它从命令行运行,特性文件也被设计成需要与被测代码一道签入版本控制系统。然而它却以帮助提高团队的业务利益相关人对开发过程的控制感为目标。当测试和开发尽情把特性塞入版本控制的时候,团队中其他人会觉得他们的文档被锁进了柜子,而他们却没有钥匙。

你的 Gherkin 特性可以充当描述新特性的设计工具,同时对系统已有的行为也是极好的参考文档。对一个具备显著规模的系统来说,没有哪个人都准确记住它在每种情况下的行为,因此,当你收到来自用户的bug报告,或者考虑为系统的某部分加入新功能的时候,自然希望这些参考就在触手可及的地方。

三位朋友

如果下面三位朋友聚到一起,将完全不同的视角融入一处,就可以创建出最好的Gherkin场景。

• 第一位朋友是测试人员,他考虑的是如何破坏东西。测试人员通常会设想大量的场景,有时能覆盖尚未理清的边界情形,有时能覆盖其他人都没想到的特别重要的情形。

• 第二位朋友是程序员,他考虑的是如何做出东西。程序员通常一边就程序的准确行为问一些澄清疑惑的问题,一边往场景中添加步骤。

• 第三位朋友是产品负责人(product owner),他关心产品的功能范围。如果测试人员想到的场景针对一种产品负责人并不关心的边界情况,他可以让团队把这一场景拿掉,或者团队可以决定只用单元测试来测。如果程序员解释说某个特定的场景实现起来会很麻烦,产品负责人有权拿掉它,或帮忙遴选替代方案。

许多实践 BDD 的团队都发现这三位朋友可以形成极好的伙伴关系来共同确定让整个团队都依赖的 Gherkin 场景。但朋友不必止于三位,比如,如果所讨论的特性会影响到团队的用户体验专家或者运营部门的同事,还可以邀请他们。

对于分享特性从而让非技术成员也能访问,Cucumber 本身只提供了有限的支持,但在Cucumber 周围,大量能够提供这种支持的插件和工具不断出现。举实来说,如果用 GitHub做版本控制,你的项目页面会显示语法高亮的特性,人们甚至可以在上面添加评论。

Relish是由来自Cucumber和RSpec团队的成员创建的一项服务,旨在提供一种方便的途径来将Cucumber特性作为文档发布。RSpec项目目前就在用自己的Relish文档作为主页,你的团队也可以这么使用。

注释:① http://relishapp.com

你只需要坚持做到同业务利益相关人一起坐下来协作编写场景,就足以收获 Cucumber至少一半的好处。这一过程所激发的交谈会解开太多太多的潜在bug或时间延误,即便你从不打算将特性自动化,也已经收获颇丰了。

然而,如果你还是希望自动化,那就继续阅读接下来的内容,看看到底怎样做才好。

自动化特性的好处在于你可以把它们作为活文档来长期信赖,因为你会将每一个场景都用于检查产品代码,以确保它们仍然有效。对于同代码打交道的程序员来说,这还有另一件好处:在他们开发系统的时候,那些测试可以充当安全网,对任何破坏已有行为的错误都给出警告。

因此,你的特性可以充当一种反馈机制,对整个团队来说提供关于系统行为的反馈,对程序员来说还能提供是否破坏已有行为的反馈。想让这些反馈循环带来好处,测试需要执行迅速,还需要可靠。我们首先来看看影响测试可靠性的问题。

Cucumber的场景从根本上讲就是状态转换测试:你将系统置于给定的(Given)状态A,执行动作 X(When),然后(Then)验证它迁移到了期望的状态 B。因此,每个场景在开始运行之前都需要系统处于某种确定的状态,而每个场景结束时也要把系统置于一种脏的新状态。

如果在两个测试之间没有重置系统的状态,我们就说状态在测试之间发生了渗露(leak)。这是导致测试脆弱的主要原因之一。

如果一个场景需依赖之前另一个场景留下的状态才能通过,就说明你在两个场景之间制造了依赖。如果有一连串的场景像火车车厢一样彼此依赖,那么离火车事故就为时不远了。

如果第一个场景,也就是正好按照下一个场景的需要将系统状态准备好的那个场景,发生了变化,那么突然之间后面那个就要失败了。即使没有改动前面一个,可如果你只想单独运行第二个场景,那又会怎样呢?没有了前一个场景渗露下来的状态,它还是会失败。

这种情况的反面,即独立的场景,可以确保场景将系统置于干净的状态,然后再在上面添加自己的数据。这使场景能够自给自足,而不是跟其他测试留下的数据或共享的固件数据耦合到一起。投入些时间精力来构造一个良好可靠的测试数据构造器的库,可以更容易达到独立场景的目标。

独立的场景对于成功自动化测试的重要性怎么强调都不为过。除了独立设置自身数据的场景所带来的附加可靠性之外,它们读起来、调试起来都更加清楚。当你仅靠阅读场景就能准确地看清它所使用的数据,而不需要在固件数据脚本中甚至更麻烦地直接在数据库中四处查阅时,理解或诊断一次失败就容易多了。

测试数据构造器

如果你使用Ruby,那肯定熟悉FactoryGirla这个gem。FactoryGirl是测试数据构造器(Test Data Builder)b这一模式的出色实现。如果你还不太了解,以下内容简单概括了它的好处。

注释:a.http://rubygems.org/gems/factory_girl

注释:b.http://www.natpryce.com/articles/000714.html

假如你正在测试一个工资单系统,作为某个场景的一部分,你需要创建一条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指向其数据库。如果不行,你还可以针对自己使用的语言找找其他类似工具。

给一个足够复杂的系统编写端到端集成测试时,你终归会遇到竞争条件和沉睡的步骤的问题。当系统的两个或多个部分并发执行,但执行是否成功需仰仗其中某一部分首先结束,这时就会发生竞争条件。就 Cucumber 测试来说,你的 When 步骤可能导致系统启动一些后台运行的工作,比如产生一份PDF或者更新一组搜索索引。如果后台任务碰巧在Cucumber执行Then步骤之前结束,场景就会通过。而如果Cucumber赢得了竞争,Then步骤在后台任务执行之前便执行了,那场景则会失败。

如果竞争双方势均力敌,你会遇上一个闪烁的场景,场景间歇地成功或失败。而如果一方胜算较多,竞争条件就会长期存在而不为人所知,直到某一天系统中某处新的变化平均了双方的筹码,场景便开始随机地失败了。

针对这种问题,一种粗糙的解决方法是在场景中引入固定时长的停顿或睡眠,从而为系统腾出时间来完成后台任务的处理。诊断竞争条件时这绝对是一种有用的短期技巧,然而,你还是应当抵住诱惑,一旦弄清了问题的原因就不要在测试中留下睡眠。引入打瞌睡的步骤不会解决场景闪烁的问题,只会让它发生的概率更低。同时,引入睡眠也会为测试的整体运行时间再增加额外的几秒,用一个问题换一个问题罢了。

如果一定要做选择,我们宁可要缓慢但可靠的测试也不要更快但不可靠的测试,不过我们没必要做这样的折中。当测试人员和程序员结对将场景自动化的时候,他们可以基于对系统工作原理的理解精心地编写测试。也就是说,他们可以利用系统中隐含的线索让测试知道何时能够安全前进,因而测试可以尽快地前进,而不必使用粗陋的定长睡眠。关于处理异步代码的示实及更多细节,可以参阅第9章。

Matt说:

fixture这一术语有不同的含义

在自动化测试领域,词语fixture至少有三种意思,这有时会引起混淆。在这一章我们使用术语固件数据(fixture data)表示用来给场景或测试用例设置上下文的数据。这是该术语在各种 xUnit测试工具a以及Ruby on Rails框架中使用的最常见的意思。

注释:a.xUnit Test Patterns [Mes07]

有一种古老的传统(源于开创测试固件这一术语的硬件世界)是将测试系统与被测系统之间的连接称为 fixture。这是“粘合代码”的角色,本书中我们称之为自动化代码(automation code)。FIT测试框架b用的就是该术语的这种含义。

注释:b.http://fit.c2.com/

一些单元测试工具(如NUnit)把水搅得更混了,它们把测试用例类本身称为fixture。关于通用语言,就说到这里吧!

在那些从手工验收测试体系转向使用自动化验收测试的团队中,共享的环境是我们经常发现的一个问题。按照传统方法,团队中的手工测试人员会使用一种称为系统测试环境的特殊环境,其中部署系统最近的构建版本。测试人员将在这一环境中运行自己的手工测试,并将bug报告给开发团队。如果有多名测试人员需要在同样的环境上运行测试,他们就需要彼此沟通,确保不会影响对方。

团队开始将测试自动化的时候,在一套新的环境中安装系统哪怕稍微有点麻烦,大家都很可能本着最小阻力的原则,把各自的测试脚本全弄到这套已有的系统测试环境中来。现在测试环境不仅在团队成员之间共享,也在测试脚本之间共享了。假设某一名开发人员收到了一个bug报告,想亲自重现一下,可他并未意识到自动化测试此时也在运行。作为bug重现步骤的一部分,开发人员无心地删除了自动化测试所依赖的一条数据库记录,自动化测试自然失败了。这种情形是导致闪烁的场景的一种典型情况。

对单一环境的共享使用还会对数据库这类炙手可热的资源造成沉重且不稳定的负荷,从而促成不可靠的测试。当共享的数据库超负荷运转时,正常情况下的可靠测试也会因超时而失败。

要解决这一问题,在新的环境中启动系统必须做到易如反掌。你需要“一键式系统搭建”,见6.3.5节。

测试人员在软件团队中常被视为二等公民。我们将在第 8章中解释,开发一组健康的Cucumber特性套件不只需要测试技巧,也需要编程技巧。如果测试人员被晾在一边独自构建Cucumber测试,他们可能不具备让步骤定义和支持代码组织良好的软件工程技巧。不知不觉中,测试会变得一团杂乱,且脆弱得没人敢去改动。

为克服这一问题,编写步骤定义和支持代码时要鼓励测试人员和程序员协同工作。程序员可以向测试人员展示如何组织代码,使之结构清晰,如何提取可重用组件和库,以供其他团队使用。有些库,比如Capybara(参阅第15章),就是在程序员从团队的步骤定义中提取可重用代码时这样产生出来的。通过与测试人员结对,程序员对如何让代码可测试也会产生更深的理解。

一个团队如果把 Cucumber 用到好处,测试人员应该能够将运行基础检查的工作托付给Cucumber。这样他们自己就可以解放出来,去做更有趣、更有创造性的探索性测试(exploratory testing)工作,就像Agile Testing: A Practical Guide for Testers and Agile Teams [CG08]一书中阐述的那样。

手工测试一套系统的时候,为它提供真实数据非常有用,这样你便如同在真实应用中使用系统。团队从手工测试转向自动测试的时候,你常常很想只移植产品数据的一个子集,从而让自动测试可以快速跟一个正常运行的系统交互。

一键式系统搭建

要避免因使用共享环境导致的闪烁的场景,团队需要一份脚本,做到只需按键一点就可以从零开始创建一份新的系统实例。

如果系统使用数据库,脚本产生的数据库应包含最新的模式(schema),以及所有的存储过程、视图、函数等。它应当仅包含系统正常运转所需的最少基础数据,比如配置数据。任何其他数据都应该等各个独立的场景自己去创建。

如果系统中有消息队列,或者 memcache 守护进程,搭建脚本也要启动它们,且使用你期望运行系统中应有的最低配置。

即使团队不在产品代码中使用Ruby,他们也可以试着用Ruby的ActiveRecorda gem来管理数据库模式和迁移脚本。ActiveRecord能让这类日常杂务变得轻而易举。

注释:a.http://rubygems.org/gems/activerecord

另一种方法,让每个测试构建自己的数据,看上去难度太大。在遗留系统中——特别是系统设计经不断发展而来的那种,创建当前测试所需的单个对象都意味着为它创建所依赖的全部对象构成的一整棵大树,你会觉得最方便的办法就是在固件数据(fixture data)中把它一次性创建好,然后与其他测试共享这棵大树。

这一方法有几个严重的问题。一组固件数据,即使开始时相对精简,时间长了体积也会只增不减。随着越来越多的场景开始依赖这些数据,且每个场景都要对它们做一点特别的处理,固件数据的体积会越来越大,复杂度也会越来越高。当你为了某个场景对数据做点改动,结果却使其他场景失败时,你将开始感受到脆弱的特性的痛苦。有这么多不同的场景依赖于固件数据,你会倾向于糊上更多的数据,因为这比修改已有的数据更加安全。某个场景失败时,你很难清楚系统中的哪一团数据可能跟失败相关,诊断起来也更加困难。

如果固件数据的集合体积庞大,在两个测试之间准备这些数据就会变慢。这会给你带来压力,诱使你编写渗露的场景,从而系统的状态不会在两个场景之间重置,因为这样速度更快。但这样做的后果我们前面已经解释过了。

我们认为固件数据是一种反模式(antipattern)。我们更倾向于使用之前介绍过的测试数据构造器,比如FactoryGirl,那样可以让测试本身从内部创建相关数据,而不是让这些数据淹没在一大团混乱的固件数据集合中。

注释:① https://github.com/thoughtbot/factory_girl

每日构建

当你碰到由大量场景所导致的缓慢的特性时,有必要考虑将构建拆成两部分。用标签来标记那些每次提交代码时都应运行的场景,其他的都降为只在每夜构建时运行。

这一模式的使用依赖于团队愿意承担风险的程度,以及犯错的趋向。降为每夜构建时运行的场景是那些极少失败甚至不失败的场景。它们针对的是那些几个月都不发生改变的功能,覆盖的是那些目前没有开发动作的稳定代码。它们是不得已时你情愿全部删除的场景。

为适当的场景打上针对签入构建的标签会带来额外的维护成本。一段时间后,这些场景中有一部分将趋于稳定,这时应该把它们降低到每夜构建中,然后用新的场景替换它们。

虽然每夜构建可以作为摆脱困境的好方法,通常来说长期的正确方案还是会打破你的大泥球(见6.3.7节)。

看起来似乎是陈述常识,可是拥有大量的场景是最容易导致特性整体运行缓慢的原因。我们并不是建议你放弃BDD并回到“牛仔式编程”(cowboy coding)当中,但我们真心建议你把缓慢的特性看做一个危险信号。除了需要很长时间等待反馈,拥有大量的测试还有其他的不利影响。数量巨大的特性很难组织得井井有条,阅读者也会觉得在其中前后翻阅极不方便。底层步骤定义和支持代码的维护同样变得更加困难。

我们发现,使用单一庞大构建的团队也更倾向于使用一种最适合用“大泥球”来描述的架构。由于系统的全部行为都实现在一个地方,所有的测试也只能放在一个地方,且只能作为一个巨块一起运行。在生存时间较长的Ruby on Rails应用中,这是一种典型的症状,这样的应用通常都经过长期持续的发展,各子系统间却没有清楚的接口。

我们会在下一节讨论更多关于如何处理“大泥球”的内容。正视这一问题,在端倪初现时一次性解决它至关重要,然而它并不是你在一夜之间就能解决的一个问题。

与此同时,你可以使用子文件夹和标签来保持特性的合理组织(参见第5章)。标签对此好处尤多,因为你可以用标签来拆分测试。你可以并发运行拆分后的不同测试集合,甚至把某些部分降为只在每夜构建(参见6.3.5节)时运行。

还有一种方法值得考虑,对于你在 Cucumber 场景中描述的行为,是否有一些可以下移一层,使用快速的单元测试来表达?热情拥抱 Cucumber 的团队有时会忘了还有单元测试这回事,而过分地依赖缓慢的集成测试来获取反馈。试着将 Cucumber 场景想象成向业务人员传达代码一般行为的粗略描述,同时仍然要靠快速的单元测试来获得尽可能高的覆盖率。实现 Cucumber 场景时让测试人员和程序员结对工作,从而协助达成这一目标。系统的某种行为需要用缓慢的端到端 Cucumber 场景来实现呢,还是应通过快速的单元测试来驱动呢?测试人员和程序员的结对可以对此做出恰当的决策。

大泥球(Big Ball of Mud)是针对某一类软件设计的一个具有讽刺意味的词语,在这种软件设计中,你实际上看不到任何人真正为软件设计做点什么。换句话说,整个软件结构就是一大团乱麻。

注释:① http://www.laputan.org/mud/

我们已经解释了大泥球会在 Cucumber 测试中引发哪些问题——缓慢的特性、固件数据和共享的环境,这些都是它可能导致的麻烦。要警惕这些信号并敢于对系统设计做出改变,从而使系统测试起来更加容易。

我们建议你把Alistair Cockburn的端口和适配器(ports and adapter)架构作为设计系统的方法,从而使系统可测试。Michael Feathers的Working Effectively with Legacy Code [Fea04]一书提供了很多实用的实子,教你如何把原先设计时没有考虑测试的大型系统分解开来。

注释:② http://alistair.cockburn.us/Hexagonal+architecture

跟团队定期召开讨论系统架构的会议:大家喜欢哪些方面,不喜欢哪些方面,又有哪些方面是大家愿意接受的。大家总是容易被雄心勃勃的想法弄得情绪高涨,可回到座位上不久那些想法就很快消失在稀薄的空气中,因此要确保讨论结束时尽量得出现实可行的步骤,从而沿着正确的方向推动大家的工作。

以上基本涵盖了你在团队中采用 Cucumber 时可能遭遇的最常见的问题。然而,理解这些问题是一回事,抽出时间来处理它们则完全是另一回事了。下一节我们就来讨论一种帮你找到合适时间的重要技巧。

在团队的所有活动中,哪一件是你觉得最重要的:为新特性编写代码?修复测试中发现的bug?修复生产环境中发现的bug?加速新特性的开发?

令人遗憾的是,在大多数软件团队的优先级列表中,测试的维护都不会出现在靠前的位置。如果办公室大楼里的升降梯坏了,可以确信立刻会有人给设备团队的人打电话。而当测试变得缓慢或脆弱时,除了依赖于它的程序员和测试人员,其他人都不会看到问题的存在。如果你多少还做些测试维护的工作,通常也只在事情坏到令你无法继续忍受,或者因为测试被严重破坏以致产品无法直接发布的时候才做。似乎总是有更重要的事情等着你做。

对测试采取这种思路的团队成员是完全错误的。对依赖于自动测试的团队来说,自动化测试就是团队的心跳,团队需要用一丝不苟的小心和关注来维持它的健康。

丰田的“停掉生产线”

在丰田的制造厂里,每次出现问题时,车间的每位工人都有权力和职责停掉整条生产线。问题马上会得到经验丰富的员工给予的全力关注,且只有等问题得到解决生产线才会重新启动。生产线重新启动之后,会有一个小组负责对问题实施根源分析(root-cause analysis),弄清发生的原因,从而从根源上解决问题。

大野耐先生第一次提出这一想法时,生产经理们认为他疯了。那时,人们觉得在制造行业最重要的事情当然是让装配线保持运行,必要时每日每夜都不间断。

注释:① Toyota Production System - Beyond Large Scale Production [Ohn88]

大野耐先生第一次向经理们提出实现这种新的系统时,有的人听从了,有的人根本不听。一开始,实施了这一策略的经理们发现他们的生产率下降了。每次遇到问题都马上停下来处理降低了它们的产量,而且当他们将产出数字同那些无视老板想法的经理们进行比较时,看上去似乎是老板错了。

然而,慢慢地那些允许生产线停下来处理每个问题的经理们开始发现他们的生产线上停工的情况越来越少了。因为每个问题都以缺陷预防的策略得到处理,这些生产线开始不断为改善机器的质量和运营的过程提供投入。很快,这些生产线的产量便大大超过了那些对貌似发疯的老板阳奉阴违的经理们所控制的生产线。那些经理们的生产线仍然沿着旧时的频率定期地哐啷罢工,被同样的老问题反复地折磨着。

缺陷预防

丰田反直觉却又取得巨大成功的“停掉生产线”策略之所以有效,是因为它是一种更全面的策略——缺陷预防——的一部分,缺陷预防关注于持续改善生产系统。若没有这种更全面的过程,“停掉生产线”的效果将微乎其微。缺陷预防过程分为以下4步。

(1)检测异常情况。

(2)停下手边的工作。

(3)修复或纠正眼下的问题。

(4)调查根源并实施对策。

这 4 个步骤非常重要,因为它能抓住手边的问题所提供的机会,从而理解过程中一些更为根本的东西。它也意味着让修正问题成为一种习惯,而不是可以推到日后不忙时的一件事情。

举个实子,假设构建被一个失败的测试破坏了。调查原因,发现有个家伙在提交代码之前没有运行所有测试,就是他提交了导致测试失败的代码。他为什么不运行所有测试呢?好吧,是因为他觉得运行测试需要太长的时间,他只运行了自认为覆盖他的改动的那部分测试,然后双手合十祷告一下便直接提交了代码。所以,底层的原因的是特性运行得太慢。理解了问题的根源之后,我们就可以着手修复它了。

有些团队维护一份构建失败的日志,其中记录着每次失败的根源。当有足够的证据证明某个特定的根源有必要处理时,他们便会集中精力来妥善处理它。

把你的团队想象成一条生产线,它为用户生产有价值的特性。如果你发现有个问题在降低它的产出速度,那就停掉生产线,把问题永久修复。实现“停掉生产线”这一策略意味着你决定把获得一套快速、高质量且可靠的测试列为整个团队的头等大事,仅次于处理影响客户的生产问题。当测试出现问题——不论是测试失败这种紧迫的问题,还是闪烁的场景这种无休止的烦恼——都要把最佳的人选推上去,永久地修复它。

Cucumber 特性对公司来说是一笔宝贵的财富。我们曾见过有团队将他们系统中大块大块的部分推倒重写,知道自己有一组准确的、可执行的规范来确保新的方案会运行得跟原来一样好,他们自不必担心什么。对这些团队来说,特性比产品代码本身更有价值。如果你打算在编写 Cucumber 特性上面做些投入,那就要照管好这些特性,让他们为整个团队带来尽可能多的益处,从而保护好这笔投入。不要迁就于缓慢的特性、间歇失败的特性或者团队中只有一部分人阅读的特性:问题一旦出现立该将其消除,让每个问题成为一个理由,基于这些理由把测试做得比以前更好。

Cucumber看上去似乎只是一种测试工具,但本质上它是一种协作工具。如果你真正努力去编写特性,使它们可作为文档提供给团队中的非技术利益相关人,你会发现自己不得不与利益相关人讨论一些原本你不会花时间讨论的细节。这些讨论揭示了他们对问题的深刻见解,这些见解将帮助你构建一套更好的方案。这才是 Cucumber 的真正奥秘:测试和文档都只是令人愉快的副作用而已,真正的价值在于交谈过程中对于知识的发掘。

尝试一下

下面是一些你可以自行尝试的练习。

在团队中实施缺陷预防

想出使团队生产线速度变慢的三件事情。每件事情的根源是什么?你可以做些什么来使它们向着好的方向变化?

关于偶然细节的练习

下面的场景是以丑陋的命令式风格编写的,穿插着各种偶然细节。

Scenario: Create an invoice

 Given I am a signed in user with role: admin

 And a client "Test Client" exists with name: "Test Client"

 And a project "Test Project" exists with:

   | name | "Test Project"|

   | client | client "Test Client" |

 And an issue "Test Issue" exists with:

   | project | project "Test Project" |

   | name| "Test Issue"|

 And a work_unit "Test Work Unit" exists with:

   | issue| issue "Test Issue" |

   | completed_on | "2011-08-24"|

   | hours| "7.5"|

 And I am on the admin invoices page

 Then I should see "Test Client"

 And I follow "Test Client"

 And I fill in "invoice_id" with "abc"

 And I press "Submit"

 Then I am on the admin invoices page

 And I should not see "Test Client"

首先弄清楚来龙去脉:你认为这个系统是做什么的?场景的用途是什么?它在测试怎样的行为?注意那些像蔓生杂草一般的偶然细节是如何妨碍你理清测试的实际目标的。

理解了场景的实质目标之后,基于自己的词汇把它重写一遍。需要的步骤应该可以少很多,但你可能会考虑使用多个场景。

用更加声明式的风格重写场景之后,你能找到原先场景中遗漏的一个关键的Then步骤吗?

相关图书

云原生测试实战
云原生测试实战
Kubernetes快速入门(第2版)
Kubernetes快速入门(第2版)
Kubernetes零基础实战
Kubernetes零基础实战
深入浅出Windows API程序设计:核心编程篇
深入浅出Windows API程序设计:核心编程篇
深入浅出Windows API程序设计:编程基础篇
深入浅出Windows API程序设计:编程基础篇
云原生技术中台:从分布式到云平台设计
云原生技术中台:从分布式到云平台设计

相关文章

相关课程