测试驱动的开发(TDD)在实践中是一个很好的思想,但有些开发人员还不能接受 “测试” 这个词所产生的概念上的骤变。在本文中,学习一种更自然的方法,将 TDD 元素整合到编程实践中。开始采用行为驱动开发(BDD)(通过 JBehave),亲身体验将注意力集中在程序行为(而不是输出)时获得的效果。
显然,测试本身是件好事。而在早期进行测试 — 例如在编写代码时 — 则更有益处,这特别有利于提高代码质量。在开发早期编写测试,您将获益良多。您能够检查代码的行为,并预先对它进行调试,这种动力无疑是巨大的。
即使了解了这种重要性,我们也没有达到关键的一点:使在编写代码之前 编写测试成为一种标准实践。正如 TDD 是极限编程(Extreme Programming)的下一个演化阶段(后者推出了单元测试框架),以 TDD 为基础,新的飞跃也将到来。本月,我邀请您和我一起实现从 TDD 到更具直观性的行为驱动测试(BDD)的演化。
行为驱动开发
虽然测试优先编程对于有些人比较管用,但是并不适用于每一个人。虽然有的应用程序开发人员狂热拥护 TDD,但也有人坚决抵制它。即使现在已经有了很多测试框架,例如 TestNG、 Selenium 和 FEST,但不对 代码进行测试的理由仍然充分。
不采用 TDD 的两个常见理由是 “没有足够的时间进行测试” 和 “代码太复杂,难以测试”。测试优先编程的另一个障碍是测试优先概念本身。很多人把测试看作一种反应型活动,仅比抽象具体一点。经验告诉我们,不能测试不存在的东西。对于某些开发人员来说,对于这种概念框架,测试优先 是一种矛盾的说法。
但是,如果不考虑编写测试和如何测试,而是考虑行为,结果会如何呢?这里所说的行为,是指一个应用程序应该 如何运行 — 实际上是指它的规范。
实际上,您已经想到了这种方法。我们都想到过。请看下面的对话。
Frank: 什么是栈?
Linda: 它是一种数据结构,按先进后出(或后进先出)的方式收集对象。它通常有一个 API,其中包括 push() 和 pop() 等方法。有时也有 peek() 方法。
Frank: push() 有什么功能?
Linda: push() 接受一个输入对象,比如说 foo,并将它放入到一个内部容器(例如一个数组)中。push() 通常不返回结果。
Frank: 如果我 push() 两个对象,比如先是 foo,然后是 bar,结果会怎样?
Linda: 第二个对象 bar 应该在栈(至少包含两个对象)的顶部,所以如果调用 pop(),那么返回的应该是 bar,而不是 foo。如果再次调用 pop(),那么应该返回 foo,然后栈为空(假设在添加这两个对象之前栈中没有对象)。
Frank: 也是说,pop 移除近放入栈中的项目?
Linda: 是的,pop() 应该移除上面的项目(假设栈中还有可移除的项目)。peek() 与此类似,只是不移除栈中的对象。peek() 应该保留栈顶的项目。
Frank: 如果之前没有 push 任何项目,那么调用 pop() 时会怎样?
Linda: pop() 应该抛出一个异常,表明栈中尚未 push 任何项。
Frank: 如果 push() null 会怎样?
Linda: 栈应该抛出一个异常,因为 null 不是一个有效的可 push() 的值。
在这段对话中,有没有注意到什么特别的地方呢(除了 Frank 不是计算机科学专业的)?这里从头到尾没有用到 “测试” 这个词。但是,“应该” 这个词却非常自然地随处闪现。
怎么做才自然?
BDD 并不是什么新生事物,更不具备什么革命性的突破。它只是 TDD 的一个分支,其中 “测试” 这个词换成了 “应该”。除了语义,很多人还发现,与测试 概念相比,应该 这个概念是一种更自然的开发驱动因素。考虑行为(应该)会自然而然地促使您先编写规范类,而后者可以成为一个非常有效的实现驱动因素。
以 Frank 和 Linda 的对话为基础,让我们看看 BDD 如何以 TDD 希望推广的方式驱动开发。
JBehave
JBehave 是用于 Java™ 平台的一个 BDD 框架,源于 xUnit 范例。正如您所料,JBehave 强调应该 这个词,而不是测试。和 JUnit 一样,您可以在自己喜欢的 IDE 中,或者通过偏爱的构建平台(例如 Ant)运行 JBehave 类。
JBehave 允许以 JUnit 的方式创建行为类;但是,在 JBehave 中,不需要扩展任何特定的基类,并且所有行为方法都需要以 should 而不是 test 开头,如清单 1 所示。
清单 1. 用于栈的一个简单的行为类
public class StackBehavior {
public void shouldThrowExceptionUponNullPush() throws Exception{}
public void shouldThrowExceptionUponPopWithoutPush() throws Exception{}
public void shouldPopPushedValue() throws Exception{}
public void shouldPopSecondPushedValueFirst() throws Exception{}
public void shouldLeaveValueOnStackAfterPeep() throws Exception{}
}
清单 1 中定义的方法都是以应该开头,它们都创建一个人类可读的句子。这里产生的 StackBehavior 类描述 Frank 和 Linda 之间的对话中提到的栈的很多特性。
例如,Linda 说,如果用户试图将 null 放到栈上,那么栈应该 抛出一个异常。查看 StackBehavior 类中的第一个行为方法:该方法的方法名为 shouldThrowExceptionUponNullPush()。其它方法的命名也遵从这一模式。这种描述性命名模式(这并不是 JBehave 或 BDD 特有的)便于以人类可读的方式报告失败行为,您很快可以看到这一点。
说到 shouldThrowExceptionUponNullPush(),那么如何验证这个行为呢?似乎 Stack 类首先需要有一个 push() 方法,这很容易定义。
清单 2. 用于探索行为的一个简单的栈定义
public class Stack<E> {
public void push(E value) {}
}
可以看到,我编写了一个简单的栈,以便首先 添加必需的行为。正如 Linda 所说,行为很简单:如果有人对 null 值调用 push(),那么栈应该 抛出一个异常。现在看看我在清单 3 中如何定义这个行为。
清单 3. 如果推出一个 null 值,则栈应该抛出一个异常
public void shouldThrowExceptionUponNullPush() throws Exception{
final Stack<String> stStack = new Stack<String>();
Ensure.throwsException(RuntimeException.class, new Block(){
public void run() throws Exception {
stStack.push(null);
}
});
}