分类: 转型, 软件测试

醒醒吧少年,只用Cucumber不能帮助你BDD

引言

在Ruby社区中,测试和BDD一直是一个被热议的话题,不管是单元测试,集成测试和功能测试,你总能找到能帮助你的工具,Cucumber就是被广泛使用的工具之一。许多团队选择Cucumber的原因是“团队要BDD”,也就是行为驱动开发(Behavior Driven Development),难道用了Cucumber之后团队就真的BDD了么?

bdd-question

事情当然没这么简单了,BDD作为一种软件开发方法论,一定要理解其含义并且遵循特定的流程,工具只不过是起辅助作用而已。会切菜的不一定都是厨子,会写代码的不一定都是程序员。近期Cucumber的作者Aslak也在博客中提到

在BDD出现的9年后,依然有不少团队在使用BDD时出现问题……BDD依然经常被人误解成单纯的测试,或者是一个可以被下载的工具。

同时,Aslak也吐槽了Cucumber目前的处境

就在最近,Cucumber已经被下载了超过500万次,我很高兴它如此受欢迎,同时也为它被广泛的误用而感到失望……Cucumber有时依然被错误的当成了自动化测试工具,而不是我当时创建的东西。

那么问题来了,怎样在日常项目中使用Cucumber呢?真的能在日常项目中进行BDD开发么?要回答这个问题,我们需要重新认识一下BDD。

BDD的提出

2003年,开发人员Dan North偶然间发现把测试的标题经过简单的文字处理可以更好表达代码蕴含的业务逻辑,比如下面这段代码,

public class CustomerLookupTest extends TestCase {
    testFindsCustomerById() {
        ...
    }
    testFailsForDuplicateCustomers() {
        ...
    }
}

当我们把测试方法中的test去掉,给单词加上空格,然后把他们组合在一起时,就会出现:

CustomerLookup
 - finds customer by id
 - fails for duplicate customers
 - ...

在Dan看来,这无疑是对CustomerLookup类的描述,并且是用测试内容来描述代码中类的行为。Dan发现他似乎找到了一种方式,可以在TDD的基础上,通过测试来表达代码的行为。在尝到甜头后,Dan写了JBehave,一个更关注代码行为的工具来代替JUnit进行软件开发。经过一番折腾后,Dan觉得只描述类行为不过瘾,便开始把关注点从类扩展到整个软件,他和当时项目组的业务人员一起把需求转化成Given/When/Then的三段式,然后用JBehave写成测试来描述软件的某种行为。当测试完成后,开发人员才开始编码,一旦测试通过,那软件就完成了测试中描述的某种行为。在他看来,他把TDD升级了,因为他不再只关注于局部类的方法,而开始关注整个软件的行为。

通过这种方式,Dan成功的把需求转换成了软件的功能测试,先写功能测试再驱动出产品代码,保证软件行为正确性。其次,Dan强调在测试中要尽可能的使用业务词汇,保证团队成员对业务理解一致。于是,BDD就此诞生

bdd-definition

BDD不只是自动化测试

在上面的故事中,“测试”这个词出现了很多次,你是不是已经认为BDD就是用功能测试驱动产品代码的开发流程呢?其实不然,功能测试只是一个结果而已,更重要的是和业务人员一起分析需求,沟通交流来产生测试的过程。用测试驱动出来的代码可以保证是正确的,但如何保证测试是正确的呢?答案就是人,通过业务,开发和测试一起参与生成的测试文档,不仅能保证软件功能上是正确的,还能保证团队成员对业务理解是一致的。在测试文档中,也应该尽量保证使用自然语言和业务词汇,减少非技术人员的学习成本。

在多年之后,Dan也终于给出了他对BDD的定义

BDD是第二代的、由外及内的、基于拉(Pull)的、多方利益相关者的(Stakeholder)、多种可扩展的、高自动化的敏捷方法。它描述了一个交互循环,可以具有带有良好定义的输出(即工作中交付的结果):已测试过的软件。

Cucumber的另一位作者Matt Wynne也给出了自己的定义

BDD的实践者们通过沟通交流,具体的示例和自动化测试帮助他们更好地探索,发现,定义并驱动出人们真正想用的软件

从上述定义我们可以看出,BDD更强调流程和一系列实践,自动化测试只是其中一部分而已。

Cucumber到底怎么用

理解了BDD的精髓后,我们就不难找出正确的使用Cucumber的方式了。根据Cucumber的定义,它的核心就是Specification,其实就是文档化的需求。Specification是通过Requrement Workshop生成的,在Workshop中业务,开发和测试一起分析需求,把需求用自然语言写成文档,然后再转换成Given/When/Then的Specification文件,这样便完成了BDD中最重要的一步,定义软件正确的行为。接着开发人员开始编码,完成相应需求,保证Specification文件运行通过,整个流程结束。

简单来说,Cucumber其实不是一个自动化测试工具,而是一个促进团队沟通合作的工具。但由于Cucumber无法确保上述流程真正的发生,有很多团队简化或者跳过了Workshop,直接开始写Specification文件,没有沟通就很难保证理解一致,Bug也许就在那时潜伏了下来。这样大家也就不难理解作者吐槽的“Cucumber被广泛的误用”,其实Cucumber只是一个沟通工具,它只是刚巧可以运行测试而已。

bdd-flow

理想很丰满,现实很骨感

任何工具和实践都有优缺点,Cucumber也不例外。团队在开始尝试新的实践或者工具时,多多少少都会碰到一些问题,下面我们就来看看一些使用Cucumber的问题。

没有业务人员参与的Specification

要么业务人员没时间写Specification,交给其他人写,写完之后业务人员也没时间去审核。在这种情况下,很难保证Specification的业务正确性,一旦Specification出现问题,团队可能发生理解不一致,甚至做错需求的现象。反过来看,Specification文件由自然语言而不是代码组成,也能反映出对非技术人员参与的重视程度。然而现实情况很难保证业务、测试、开发有充足的时间进行Specification的讨论和编写,这也是导致业务人员逐渐脱离Specification的主要原因。

Specification关注实现细节而不是业务逻辑

Cucumber使用自然语言描述业务需求,然而不少团队都陷入到了实现细节中。比如

Scenario: Detect agent type based on contract number

Given I am on the 'Find me' page
And I have entered a contract number
When I click 'Continue' button
And a contact number match is found
Then the "Back" button will be displayed

上面的描述满篇是点击了那个按钮,输入了什么内容,看完之后反而让人有点困惑,用户到底为什么要做这些,做了之后有什么价值。这样的Specification既不能满足团队成员对业务需求的了解,也会由于界面的细微改动运行失败。

Step的嵌套调用

Specification文件由Step组成,在Step中我们可以通过Ruby进行自动化的页面操作。有时我们会发现某些Specification会重复进行一系列的操作,这时我们就可以把重复的Step进行组合,创建出新的Step。比如这样

Given there is student Harry
And there is professor Snape
And student Harry joins class of professor Snape

# use 1 new step instead of 3
Given student Harry in class of professor Snape

那么这个新的Step该怎么实现呢?Cucumber支持在Step中调用Step,比如这样

Given /^student (.*) in class of professor (.*)/ do |student, professor|
    step "there is student #{student}"
    step "there is professor #{professor}"
    step "student #{student} joins class of professor #{professor}"
end

乍一看好像没什么问题,其实不然。Step使用正则表达式进行匹配,问题恰恰出在正则上。

首先,正则灵活性很大,你确定上面例子中step “there is student #{student}”一定会调用到你想要调用的Step么?你无法确定在运行时,是否会出现另一个Step “there is student come from China”来截胡。

其次,正则逆推难度很大,也就是说当你看到“^(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])$”时,你很难看出这是在匹配IP地址。所以当我们需要修改step时,很难确定有多少个step在依赖它,这也加大了维护成本。

最后,嵌套次数过多的Step也会导致代码复杂,难以理解。

Specification Report可读性不高

Specification除了是自动化测试的描述文件之外,更重要的是软件的“活文档”。有时我们需要通过“活文档”进行知识传递。Cucumber虽然提供生成Report功能,但效果未免有些差强人意。比如下面

bad-report

满篇绿色的Step,再加上Given/When/Then来捣乱,这样的Report只是运行结果而已,可读性很差,很难当成软件需求文档。究其原因,主要因为Cucumber Report的表现力差。

首先,它只支持纯文本,在这个“一图胜千言,无图无真相”的时代很难只通过文字来描述复杂业务,如果能在文档中加上图片,甚至一段视频,都会帮助我们更容易的理解复杂业务。比如像下面这样的。

nice-report

其次,Cucumber Report关注的更多是Step,而不是软件需求。当我们想到软件文档或者手册时,我们脑海中想到的更多是像教科书一样的文档,内容之间有层级和关联关系,每个功能有重点和概要内容,更偏向自然语言,而不是简单的把Specification堆在一起,满篇的Given/When/Then。在现实情况下,这样的Cucumber Report也难免没有人愿意阅读了。

改进措施

遇到上面的问题不要怕,我们只要理解问题本质,找到对策解决它,依然可以帮助我们更好地完成任务。下面我们就来尝试解决一下上面提到的三个问题。

让业务人员写/审查Specification

对于上面的关注细节的例子,如果我们换一个思路,不去考虑UI之类的东西,就会得出更精炼的Specification。比如下面

Scenario: Customer has a tied agent policy 
so last name is required

Given I have a "TiedAgent" policy
When I submit my policy number
Then I should be asked for my last name

这样的Specification不再关注按钮,而是具体的业务需求,这样就把细节的UI操作推向了Cucumber Step的实现中。这样可以更直接的展现需求,避免细节内容的干扰。如果情况允许,我更支持让业务人员写Specification,或者最起码也要审查Specification文件。通常业务人员是团队中最不懂技术的,这反而是他们的优势,可以把Specification变得更加面向需求,更加通俗易懂。

Step实现代码的重用

我们可以通过重构Step实现代码来进行有效的重用,比如下面

Given /^there is student (.*)/ do |student|
    ModelFactory.create_user(student)
end

Given /^there is professor (.*)/ do |professor|
    ModelFactory.create_user(professor)
end

Given /^student (.*) joins class of professor (.*)/ do |student, professor|
    ModelFactory.join_class(student, professor)
end

通过重构,我们抽象出ModelFactory进行相关数据准备,然后就可以重用ModelFactory实现新的Step

Given /^student (.*) in class of professor (.*)/ do |student, professor|
    ModelFactory.create_user(student, professor)
    ModelFactory.join_class(student, professor)
end

重用代码而不是重用Step,这样不仅可以让Step的实现代码更加简洁,同时也避免了Step的嵌套调用。

扩展Cucumber生成高质量的文档

Cucumber虽然自带不少种格式的Report,但都不能称其为真正的文档。不过我们可以通过扩展Cucumber来生成高质量的文档。

首先,我们可以使用Capybara在对某个正在执行的Step进行截图。Capybara提供的截图功能可以保留当前Step的运行状态,通过图片更容易理解当时的上下文,这些图片拼在一起,其实就是一个完整的用户操作流程。

其次,我们可以通过给Step添加详细描述来解决Report不给力的问题。我们可以给每一个Specification文件创建一个相对应的描述文件,描述文件由两部分组成,一部分是Step的标题,另一部分是详细描述Step的内容。只要通过文本匹配就可以找到某个Step的详细描述,再加上之前对Step的截图,拼在一起就可以生成一个高质量的文档了。

举个例子,如果我们有这样一个Specification文件

Scenario: Customer has a tied agent policy 
so last name is required

Given I have a "TiedAgent" policy
When I submit my policy number
Then I should be asked for my last name

创建一个对应的描述文件,文件类型是Markdown。

=====================
Given I have a "TiedAgent" policy
=====================
##Detail Information**
**In this step, you are assigned a "TieAgent" policy.**
![screenshot-1](./i-have-a-tied-agent-policy.png)
You can click [here](http://example.com) for more information
=====================
....

在上面的文件中,第一部分是Step标题,用来匹配Specification文件中的Step,第二部分是Markdown类型的片段,里面有图片,超链接等富文本元素,可以更好地帮助我们理解业务。

最后,通过Cucumber中提供的AfterStep Hook完成文档的生成。比如这样

AfterStep('@active-doc') do |scenario|
    @step ||= 0
    @doc ||= ActiveDocument.new
    @doc.generate(scenario, scenario.steps[@step].name)
    @step += 1
end

代码中的ActiveDocument是自己实现的,它把丰富的HTML内容和截图整合在一起,然后把Specification中所有的Step拼接在一起,就生成了一个Specification的文档。这样的文档相比之前提到的Cucumber Report具有更高的可读性,同时也具有更强的灵活性,因为文档是通过HTML展现的,我们可以添加更多的内容,比如Specification文档之间的跳转链接,或者提前录制的一段视频放入文档中,甚至可以加上第三方css和js库让文档变得更加引人入胜。

active-doc

原来生活可以更美的

随着BDD的发展,越来越多的工具进入了我们的视野。我们应该认清团队的需求,结合团队的特点选择合适工具,不要盲目的随大流。下面我来列举一些具有代表性的工具,推荐给不同类型的团队。

Cucumber

简单来说,Cucumber实际上是一款有一定文档性,可以帮助团队沟通合作的,提供自动化测试功能的工具。特点是上手简单,社区活跃,文档表现力不足。所以如果团队刚开始尝试BDD,更看重在自动化测试方面,而对需求文档化要求不高时,Cucumber是一个不错的选择。同时Cucumber目前支持Ruby, C#, JVM, JS和C++,众多平台也是一个加分项。

Concordion

与Cucumber相比,Concordion提供了更好的文档支持。Concordion的Specification是HTML格式的,我们再也不用生搬硬套的使用Given/When/Then进行功能描述了。在HTML文件中,我们可以更加自由的描述业务需求,同时可以增加好看的样式,添加更友好的交互,放入更多的视频和图片等等。总而言之一句话,HTML比纯文本更加灵活强大,适合阅读。同时我们也要清楚HTML的学习和维护成本相比纯文本更加昂贵,非技术的人可能很难单独完成。和技术人员结对完成,或者在技术人员完成后进行审查也是一个不错的选择。但由于Concordion目前只对C#和JAVA支持较好,所以如果团队刚好用到C#和Java,并且非常看重文档化需求,那么Concordion要比Cucumber更加适合你们。在下面的例子中,我们使用Concordion生成了“教学评估”相关的需求文档,并且使用了shower.js增强了用户交互,在保证软件功能的同时,带来了更好的阅读体验。

交互性更好地需求文档,内容组织合理,阅读体验好。

concordion-index

其中一个需求的详细描述,同时也是自动化测试。

concordion-one-example

Gauge

Gauge也在文档方面进行了改善,Gauge的Specification文件由Markdown组成,相对纯文本有了一定程度的提升,但还是不如Concordion灵活。Gauge使用Go编写,天然支持并发运行,相比之下性能要更加有优势。同时Gauge支持多语言实现,目前支持Java,C#和Ruby,相比Cucumber在跨平台式需要整个切换工具,Gauge更容易做跨平台。虽然目前Gauge处在开发阶段,但依然值得关注。

gauge

总结一下

  1. BDD不是工具,而是一套流程和一系列实践。它需要团队成员的通力合作,可以帮助整个团队更好的理解业务,理解软件。
  2. Cucumber作为支持BDD的一种工具,不单单是自动化测试工具。在解决了Cucumber的一些问题后,团队可以更加有效的使用。
  3. Cucumber,Concordion和Guage各有不同,选择一款适合团队自身需要的工具,也能保证团队顺利运作,少走弯路。

好了少年,我只能帮你到这里了,接下来BDD之路就看你自己的了。

点击这里浏览演讲视频。

发表评论

评论