本文翻译自Dan North的文章"Introducing BDD"。

我遇到了一个问题。当我在不同环境的多个项目中使用和教授类似测试驱动开发(test-driven development, TDD)这样的敏捷实践时,我总是能遇到来自程序员们相同的困惑和误解。他们想知道从哪里开始、测什么不测什么、一次测试多少、谁来调用他们的测试以及如何理解为什么一个测试失败了。

越是深入TDD,我越能感觉到我对TDD认知过程是时断时续、逐步掌握的,还远未进入到死胡同。我记得多数时间我想到的都是"这只是别人告诉我这样做的",而不是"哇,我明白为何要这样做了"。我断定一定可以通过某种方法将TDD直截了当地呈现给那些优秀的程序员们,并且可以避免所有陷阱。

我给出的答案是行为驱动开发(Behaviour-drive Development, BDD)。它从已有的敏捷实践演化而成,其设计目的是让敏捷实践对于采用敏捷软件交付的新团队来说变得更加容易理解和高效。随着时间推移,BDD已经发展为一种包含敏捷分析以及自动验收测试的敏捷实践。

测试方法名应该成句

我第一次发出"Aha!"是当看到我的同事Chris Stevenson开发的一款看似简单的名为agiledox的工具程序时。这个程序用于处理JUnit的测试类,并以普通句子的形式打印出方法名。其中的一个测试用例看起来像这样:

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

结果是这样的:

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

"test"这个词被从类名和方法名中剥离出来,采用驼峰式命名方式(camel-case)的方法名被转换为普通的文本。这就是这个工具所做的一切,但是它产生的效果却是惊人的。

开发人员发现这至少可以为他们产生一些文档,所以他们开始编写使用真实句子作为名字的测试方法。更重要的是,当他们使用业务领域的语言作为方法名后,生成的文档对于商业用户、分析师以及测试人员变得同样有意义了。

一个让你专注于测试方法的简单句子模板

接下来我无意中发现了以单词"should"作为开头的测试方法命名手法。这个句子模板-"这个类应该(should)做某事

"-意思是你只能为当前类定义测试,这会让你保持专注。如果你发现自己编写了一个名字不符合该模板的测试,这表明这个行为很可能属于其他地方。

例如,我正在编写一个用于校验屏幕输入的类。大多字段都是常规的客户信息-名,姓氏等等,不过其中有一个字段用于输入出生日期,还有一个字段用来输入年龄信息。我开始编写一个ClientDetailsValidatorTest类,其中包含诸如testShouldFailForMissingSurname和testShouldFailForMissingTitle的测试方法.

接下来,我开始着手计算年龄,我的思维进入了一个充斥着繁琐业务规则的世界:如果客户同时提供的年龄和出生日期信息两者无法匹配该怎么处理?如果提供的出生日期是今天呢,又该如何处理?如果我只得到了出生日期,我应该如何计算年龄呢?为了描述这个行为,我正在编写的一些测试方法名字日益复杂,所以我考虑将其交给其他类去处理。这促使我引入了一个名为AgeCalculator的新类以及对应的AgeCalculatorTest。所有有关年龄计算的行为都放到calculator这个类中,这样validator类只需要一个有关年龄计算的测试例,并保证其与calculator类可以正确地交互。

如果一个类做了不止一件事情,这对我而言通常是一个提示:我应该引入其他类来分担一些工作了。我会将该新服务定义成一个可以描述它自身职责
的接口,并且将该服务通过类的构造函数传入:

public class ClientDetailsValidator {
 
    private final AgeCalculator ageCalc;
 
    public ClientDetailsValidator(AgeCalculator ageCalc) {
        this.ageCalc = ageCalc;
    }
}

这种将众多对象连接在一起的手法,即通常所说的依赖注入(dependency injection),在与mock机制一同使用时特别有用。

当测试失败时,一个表达良好的测试名字十分有用

不久,我就发现如果我修改代码后导致测试失败,我可以查看测试方法的名字并识别出这段代码预期的行为。通常发生的情况有下面三种:

* 我引入一个bug。都怪我。解决方法:修正这个bug。
* 预期行为仍然有意义,但已移至别处了。解决方法:将这个测试例移走,也许还要进行一些修改。
* 这个行为已经不再正确 – 系统的前提发生了改变。解决方法:删除这个测试。

在敏捷项目中随着你的理解的深入,最后一种情况很可能发生。不幸的是,TDD新手对删除测试例有着与生俱来的恐惧,就好像这样做会降低他们的代码质量似的。

在一个更微妙的方面上,与那些更加正式的单词will或shall相比,单词should的含义更加显而易见。Should隐式地允许你挑战测试例的前提:"它应该吗?果真是这样吗"。这样我们可以更加容易判断出测试失败到底是由于你引入的一个bug还是只是因为你之前对系统行为的假设已经不再正确。

与"测试(test)"相比,"行为(Behaviour)"是个更加有用的词

现在,我有一个工具 – agiledox – 用来删除单词"test",并且我拥有一个编写测试方法名的模板。我突然意识到人们关于TDD的误解几乎都归结到单词"test"上。

这并不是说测试不是TDD所固有的 -测试方法的结果集是一个保证你的代码可以正确工作的有效途径。但是,如果方法没有全面地描述你的系统的行为,那么它们会使你产生一种虚假的安全感。

在进行TDD时,我开始使用单词"behaviour"代替"test"。我发现这样做不仅合适,而且之前在TDD辅导时遇到的各类问题也都迎任而解。现在我已经有了这些问题的答案。什么来调用你的测试,这个问题变得很容易回答 – 我们在一个句子中调用你的测试,这个句子描述了你感兴趣的行为。测试多少用例才算充分 – 这取决于你在一个句子中可以描述多少行为。当测试失败时,我们可以简单地按照上面描述的过程解决- 要么你引入了一个bug,要么这个行为被移走了,或者是这个测试不再有意义了。

我发现这种从考虑测试到考虑行为的思维转变影响是如此巨大,以致于我开始把TDD称作为BDD,或行为驱动开发了。

与测试相比,JBehave更强调行为

在2003年末,我觉得是时候采取实际行动了。我开始编写一个名为JBehave的JUnit的替代品,它删除了代码中所有涉及测试的词汇,并替换为与验证行为相关的词汇。我这样做的目的就是为了看看如果我严格坚持我的行为驱动方法,这样一个框架将会如何演变。同时,我认为这也是一个有价值的教学工具,可以用来介绍TDD和BDD,同时可以避免大家分心于那些Test相关的词汇。

为了定义一个假想CustomerLookup类的行为,我编写了一个行为类,例如,CustomerLookupBehaviour。这个类包含以单词"should"开始的方法。行为运行器(behaviour runner)将实例化这个行为类,并依次调用每个行为方法,就像JUnit处理测试例的方式一样。它还会报告执行进度并在结束时输出一份总结。

我的第一个里程碑是使得JBehave做到自我验证。我只不过增加了一些行为,使得JBehave可以验证自己。我能够将所有JUnit测试例移植为JBehave行为并且可以像JUnit那样立即获取验证结果的反馈。

确定最重要的行为

接下来,我发现了商业价值的概念。当然了,我一直知道我编写软件的原因,但是我从未真正考虑过我现在所编写代码的价值。我的另外一个同事,业务分析师Chris Matts,促使我开始考虑在行为驱动开发背景下的商业价值。

假定我在头脑中已经有了使JBehave自托管的目标,我发现一个真正有用的保持专注的方法就是问:系统尚未
实现的最重要的特性是什么

这个问题需要你能识别出你尚未实现的特性的价值,并按优先级顺序对它们进行排序。它也可以帮助你制定这个行为方法的名字:系统尚未实现X(X是一个有意义的行为),X是重要的,这意味着系统应该实现X;所以你的下一个行为方法很简单:

public void shouldDoX() {
    // …
}

现在我有了另外一个TDD问题即"从何开始"的答案了。

需求也是行为

此时此刻,我拥有了一个框架,它可以帮助我理解,并且更重要的是解释TDD是如何工作的,并且还可以帮助我解释一种避免我遇到的所有陷阱的方法。

临近2004年年底,当我向Matts描述我新发现的、基于行为的词汇时,他说"但是这很像分析"。当我们讨论到这些时,我们停顿了很长时间,然后我们决定将这种行为驱动的思维方式应用于定义需求。如果我们可以为分析师、测试人员、开发人员以及业务开发出一致的词汇,那么我们就可以很好的消除技术人员和业务人员沟通过程中产生的一些岐义和错误传达。

BDD为分析提供了一种"通用语言(ubiquitous language)"

就在此期间,Eric Evans出版了他的畅销书《领域驱动设计》。在书中,他使用一种基于业务领域的通用语言描述了系统建模的概念,这使得商业词汇渗透到了代码库中。

Chris和我意识到我们正试图为分析过程本身
定义一种通用语言!我们拥有一个很好的起点。公司内部已经有了一个常用的故事模板,看起来类似这样:

作为(As a)
[X]
我要(I want)
[Y]
结果是(so that)
[Z]

这里Y是某个特性,Z是这个特性的价值或带来的益处,X是这个特性的受益人或角色。它的优点在于当你第一次定义需求故事时,它将迫使你识别交付这个故事的价值。当一个故事没有真正的商业价值,它常常可以归结为类似:"…我想要[某个特性],所以[我就去做,好吗?]"。这样可以更加容易地消减一些难懂的需求。

从这点触发,Matts和我开始着手了解每个敏捷测试人员已经知道了些什么:一个故事的行为仅仅是其验收标准 – 如果系统满足所有验收标准,它的行为就是正确的;相反,它的行为就是不正确的。所以我们创建了一个模板来捕捉一个故事的验收标准。

这个模板应该足够宽松,这样分析师们不会感觉到矫揉造作或受到约束。不过它也应该足够结构化,这样我们可以将故事分解成组成片断并自动生成它们。我们从场景(scenarios)的角度来描述验收标准,采用如下形式:

假定(Given)
一些初始上下文,
当(When)
一个事件发生,
那么(then)
要保证一些结果。

为了说明这一点,我们使用ATM机这个经典的例子。其中的一个故事卡可能看起来像这样:

+标题: 客户取现金+
作为(As)一个客户
我想(I want)从一台ATM机中取现金
结果(so that)是我不需要在银行中排队等候

那么我们怎么知道何时我们已经交付了这个故事呢?这里有几种场景要考虑:账户可能有盈余,账户可能被透支,但在透支额度以内,账户可能被透支且超出透支额度。当然,还有其他一些场景,注入如果账户有盈余,但是这次取款将使得账户透支,或如果自动取款机现金量不足。

使用given-when-then模板,头两个场景可能看起来是这样的:

+场景 1: 账户有盈余 +
假定账户有盈余
并且(And)卡片是有效的
并且取款机有现金
当(When)客户请求现金时
那么(Then)要保证这个账户被记入了贷方
并且(And)保证现金被取出
并且保证卡片被返还

注意,and用于以自然的方式连接多个givens(假定)或多个outcomes(结果)。

+场景 2: 账户透支超出额度限制+
假定账户被透支
并且卡片是有效的
当客户请求现金
那么要保证显示一条拒绝消息
并且保证现金没有被取出
并且保证卡片被返回

两个场景都是基于同样的events(事件),甚至有一些共同的givens和outcomes。我们要通过重用givens,events和outcomes充分利用这一点。

验收标准应该是可执行的

场景的片断-givens,events和outcomes-的粒度足够细,可以直接用代码表示。JBehave定义了一个对象模型,该模型允许我们直接将场景片断映射为Java类。

你编写一个类用于代表每个given:

public class AccountIsInCredit implements Given {
    public void setup(World world) {
        …
    }
}
public class CardIsValid implements Given {
    public void setup(World world) {
        …
    }
}

并且另外一个用于代表event:

public class CustomerRequestsCash implements Event {
    public void occurIn(World world) {
        …
    }
}

outcomes也是这样。然后JBehave将所有这些联系起来并且执行它们。它创造了一个"世界",只是用于存储你的对象,它将这个世界依次传递给每个givens,这样这些givens就可以用已知状态生存于这个世界中了。JBehave接下来告诉events出现在这个世界,它们实现了场景的实际行为。最后,它将控制权传递给我们为这个故事定义的任一一个outcome。

用一个类来表示每个片断使得我们可以在其他场景或故事中重用这些片断。起初,我们通过使用mock机制设置账户有盈余或者卡片有效来实现片断。这些形成了实现行为的起始点。当你实现应用时,你修改givens和outcome,使用你实现的实际类,这样直到场景完成为止,他们已经成为正确的端到端的功能测试。

BDD的现在和未来

经过一次简短的停顿后,JBahave回归到积极的开发。其核心已经相当完整和健壮了。下一步是将其与流行的Java IDE如IntelliJ IDEA和Eclipse集成在一起。

Dave Astels一直在积极推动BDD。他的博客以及各类发表的文章引发了一系列活动,最引人注目的是rspec项目,它是一个用Ruby语言实现的BDD框架。我已经开始开发rbehave,它将是一个用Ruby实现的JBehave。

我的许多同事都一直在现实世界中的各种项目中使用了BDD技术,并且发现这个技术非常成功。JBehave的故事runner – 校验验收标准的部分 – 正在积极的开发中。

我们的目标是拥有一个往返的编辑器,这样业务分析师和测试人员可以在一个普通的文本编辑器中捕获故事,同时这个编辑器还可以为行为类生成桩代码,所有这些都使用业务领域的语言描述。BDD的演化是与大家的帮助分不开的,我在这里十分感谢他们。

© 2011, bigwhite. 版权所有.

Related posts:

  1. Effective Java阅读笔记-item16
  2. 认识Mock Object
  3. 开放与封闭
  4. Effective Java阅读笔记-item18
  5. 再谈Mock Object