6.2 同心协力
6.2.1 偶然细节
考虑下面这个为在线邮件客户端编写的场景:
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)2。这种不相关的细节使得场景很难阅读,而这又会让利益相关人对阅读场景失去兴趣。我们去掉密码,把场景重写一下:
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)想让我们修改身份验证机制,我们只需修改下层的步骤定义代码,而不必去动特性。
避免偶然细节
如果你是一名程序员,或许早已熟练了每天在阅读代码的时候滤掉不相关的细节。编写场景的时候更要时刻想着这一点,因为一不留神这些偶然细节就会溜进来。
编写场景的时候,尽力避免被已有的步骤定义所左右,只管用直白的英语把你希望发生的事情确切地写下来即可。事实上,尽量不要让程序员或测试人员独自编写场景。让非技术的利益相关人或者分析师从纯粹以业务为中心的角度出发编写每个场景的初稿,或者最理想的情况是与程序员结对,从而分享他们的构思模型。有了工程意义上设计精良的支持层,你便可以信心百倍地快速编写新的步骤定义来配合场景的表达方式。
6.2.2 命令式步骤
要让计算机为你做事,你需要给它提供指令,在计算机程序设计中,关于如何表达这些指令有两种对比鲜明的风格,分别称为命令式编程(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章向你展示这种做法。
6.2.3 重复
所有好的计算机程序员都明白重复对于代码的可维护性有多大的害处,然而我们还是经常看到团队的Cucumber特性中充满了重复。重复显然会让你的场景脆弱不堪,此外也让场景读起来单调乏味。
我们在第5章中演示过,Gherkin提供了可用来减少重复的Background和Scenario Outline关键字,但有时重复是一个信号,说明编写步骤所用的抽象层次太低,要对这种情况保持警觉。最好跟团队中的非技术成员一道工作,获得他们的反馈:哪种重复他们可以接受,哪种重复会让他们目光呆滞。
6.2.4 语言不通用
团队使用的通用语言将由系统涉及的领域来驱动。如果你们在构建面向现场音乐爱好者的系统,通用语言将包含音乐会、演出、表演者和场地之类的词语。如果你们在做电视节目预告,通用语言中将有播音员、节目类型、节目长度和播出日期之类的词语。
关键在于团队的所有成员在所有场合都使用同样的词语。如果一个数据库表名叫tbl_Performer,而其中的数据行所表示的东西被团队中多数人称为artists,那是不可接受的。每当出现这样的术语分歧的时候,大家应该马上停下来,确定哪个才是应该使用的,适当纠正后就坚持使用它。
我们讨论的是开发一种通用语言,因为这是一个持续进行的过程。这一开发过程需要我们工作上的投入。真正做到彼此倾听并且就使用的词语达成一致是需要努力的,而坚持这种约定也是需要纪律的。
然而,回报是巨大的。使用通用语言的团队犯错更少,且更能享受工作的乐趣,因为他们能有效地沟通工作内容。不重视通用语言价值的团队将会粗枝大叶地使用场景中的语汇,从而丧失在团队中专注技术和专注业务的双方之间构筑坚固桥梁的宝贵机会。那时,如果你试图纠正别人或澄清术语,带给人的感觉只会是吹毛求疵。
花点时间向团队解释一下通用语言的概念及益处。一旦大家都理解它为什么重要,你会发现大家会更乐于花精力讨论并决定使用哪些词语更合适。
如果用得正确,Cucumber可以帮助团队把通用语言开发出来。在程序员和业务人员协同编写场景的时候,你会发现关于用词精确性的各种争论会时不时地暴发。非常好!每一次分歧都暴露了两班人马之间一处潜在的误解,或者说一个bug。对新团队来说,这样的讨论开始会困难一些,但随着这一语言的不断开发,事情将变得越来越容易。下面的“三位朋友”提供了组织这种会议一种好方法。
6.2.5 闭门造车式的特性
人们会觉得Cucumber是一种技术性较强的工具。它从命令行运行,特性文件也被设计成需要与被测代码一道签入版本控制系统。然而它却以帮助提高团队的业务利益相关人对开发过程的控制感为目标。当测试和开发尽情把特性塞入版本控制的时候,团队中其他人会觉得他们的文档被锁进了柜子,而他们却没有钥匙。
你的Gherkin特性可以充当描述新特性的设计工具,同时对系统已有的行为也是极好的参考文档。对一个具备显著规模的系统来说,没有哪个人都准确记住它在每种情况下的行为,因此,当你收到来自用户的bug报告,或者考虑为系统的某部分加入新功能的时候,自然希望这些参考就在触手可及的地方。
对于分享特性从而让非技术成员也能访问,Cucumber本身只提供了有限的支持,但在Cucumber周围,大量能够提供这种支持的插件和工具不断出现。举例来说,如果用GitHub做版本控制,你的项目页面会显示语法高亮的特性,人们甚至可以在上面添加评论。
Relish4是由来自Cucumber和RSpec团队的成员创建的一项服务,旨在提供一种方便的途径来将Cucumber特性作为文档发布。RSpec项目目前就在用自己的Relish文档作为主页,你的团队也可以这么使用。
你只需要坚持做到同业务利益相关人一起坐下来协作编写场景,就足以收获Cucumber至少一半的好处。这一过程所激发的交谈会解开太多太多的潜在bug或时间延误,即便你从不打算将特性自动化,也已经收获颇丰了。
然而,如果你还是希望自动化,那就继续阅读接下来的内容,看看到底怎样做才好。