追求代码质量 – 亲身体验行为驱动开拓
副标题#e#
显然,测试自己是件功德。而在早期举办测试 — 譬喻在编写代码时 — 则 更有益处,这出格有利于提高代码质量。在开拓早期编写测试,您将获益良多。 您可以或许查抄代码的行为,并预先对它举办调试,这种动力无疑是庞大的。
纵然相识了这种重要性,我们也没有到达要害的一点:使在编写代码之前 编 写测试成为一种尺度实践。正如 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 不是计较机科 学专业的)?这里从新到尾没有用到 “测试” 这个词。可是, “应该” 这个词却很是自然地到处闪现。
怎么做才自然?
我应该利用哪种框架?
由于注释(annotation)的缘故,可以使 用 JUnit 和 TestNG 来实践 BDD。我发明利用 JBehave 之类的 BDD 框架越发 有趣,因为它提供了界说行为类的特性,譬喻异常框架 便于实现更具文学气势气魄 的编程。
BDD 并不是什么新闹事物,更不具备什么革命性的打破。它只 是 TDD 的一个分支,个中 “测试” 这个词换成了 “应该 ”。除了语义,许多人还发明,与测试 观念对比,应该 这个观念是一种 更自然的开拓驱动因素。思量行为(应该)会自然而然地促使您先编写类型类, 尔后者可以成为一个很是有效的实现驱动因素。
以 Frank 和 Linda 的 对话为基本,让我们看看 BDD 如何故 TDD 但愿推广的方法驱动开拓。
#p#副标题#e#
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{}
}
#p#分页标题#e#
清单 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);
}
});
}
精巧的 expectation 和 override
在清单 3 中产生的一些工作是 JBhave 特有的,所以要表明一下。首先,我 建设 Stack 类的一个实例,并将它限制为 String 范例(通过 Java 5 泛型) 。接下来,我利用 JBehave 的 异常框架 实际建模我所期望的行为。Ensure 类 雷同于 JUnit 或 TestNG 的 Assert 范例;可是,它增加了一系列要领,提供 了更具可读性的 API(这常被称作文学编程)。在清单 3 中,我确保了假如对 null 挪用 push(),则抛出一个 RuntimeException。
JBehave 还引入了一个 Block 范例,它是通过用所需的行为包围 run() 方 法来实现的。在内部,JBehave 确保期望的异常范例不被抛出(并因此被捕获) ,而是生成一个妨碍状态。您大概还记得,在我前面关于 用 Google Web Toolkit 对 Ajax 举办单位测试 的文章中,也呈现了雷同的包围便利类的模式 。在那种环境下,包围是通过 GWT 的 Timer 类实现的。
假如此刻运行清单 3 中的行为,应该看到呈现错误。凭据今朝编写的代码, push() 要领不执行任何操纵。所以不行能生成异常,从清单 4 中的输出可以看 到这一点。
清单 4. 没有产生期望的行为
1) StackBehavior should throw exception upon null push:
VerificationException: Expected:
object not null
but got:
null:
清单 4 中的句子 “StackBehavior should throw exception upon null push” 模仿行为的名称(shouldThrowExceptionUponNullPush()),并加上类 的名称。实际上,JBehave 是在陈诉当它运行所需的行为时,没有得到任何回响 。虽然,我的下一步是要使上述行为乐成运行,为此我查抄 null,如清单 5 所 示。
清单 5. 在栈类中增加指定的行为
public void push(E value) {
if(value == null){
throw new RuntimeException("Can't push null");
}
}
当我从头运行行为时,一切都运行得很好,如清单 6 所示。
清单 6. 乐成!
Time: 0.021s
Total: 1. Success!
行为驱动开拓
清单 6 中的输出与 JUnit 的输出是不是很像?这也许不是巧合,对差池? 如前所述,JBehave 是按照 xUnit 典型建模的,它甚至通过 setUp() 和 tearDown() 提供了对 fixture 的支持。由于我大概在整个行为类中利用一个 Stack 实例,我大概也会将那种逻辑推入(这里并非有意利用双关语)到一个 fixture 中,正如清单 7 中那样。留意, JBehave 将与 JUnit 一样遵循沟通 的 fixture 法则 — 也就是说,对付每个行为要领,它都运行一个 setUp() 和 tearDown()。
清单 7. JBehave 中的 fixture
public class StackBehavior {
private Stack<String> stStack;
public void setUp() {
this.stStack = new Stack<String>();
}
//...
}
#p#分页标题#e#
对付接下来的行为要领,shouldThrowExceptionUponPopWithoutPush() 暗示 我必需确保它具有雷同于 清单 3 中的 shouldThrowExceptionUponNullPush() 的行为。从清单 8 中可以看出,没有任何出格神奇的处所 — 有吗?
清单 8. 确保 pop 的行为
public void shouldThrowExceptionUponPopWithoutPush() throws Exception{
Ensure.throwsException(RuntimeException.class, new Block() {
public void run() throws Exception {
stStack.pop();
}
});
}
您大概已经清楚地知道,此时清单 8 并不会真正地编译,因为 pop() 还没 有被编写。可是,在开始编写 pop() 之前,让我们思量一些工作。
确保行为
从技能上讲,在这里我可以将 pop() 实现为无论挪用顺序如何,都只抛出一 个异常。可是当我沿着这条行为蹊径前进时,我又忍不住思量一个支持我所需要 的类型的实现。在这种环境下,假如 push() 没有被挪用(可能从逻辑上讲,栈 为空)的环境下确保 pop() 抛出一个异常,则意味着栈有一个状态。正如之前 Linda 思考的那样,栈凡是有一个 “内部容器”,用于实际持有项目。相应地 ,我可觉得 Stack 类建设一个 ArrayList,用于保持通报给 push() 要领的值 ,如清单 9 所示。
清单 9. 栈需要一种内部的方法来持有工具
public class Stack<E> {
private ArrayList<E> list;
public Stack() {
this.list = new ArrayList<E>();
}
//...
}
此刻我可觉得 pop() 要领编写行为,即确保当栈在逻辑上为空时,抛出一个 异常。
清单 10. pop 的实现变得更容易
public E pop() {
if(this.list.size() > 0){
return null;
}else{
throw new RuntimeException("nothing to pop");
}
}
当我运行清单 8 中的行为时,一切如预期运行:由于栈中没有存在任何值( 因此它的巨细不大于 0),于是抛出一个异常。
接下来的行为要领是 shouldPopPushedValue(),这个行为要领很容易指定。 我只是 push() 一个值(“test”),并确保当挪用 pop() 时,返回沟通的值 。
清单 11. 假如将一个值入栈,那么出栈的也应该是它,对吗?
public void shouldPopPushedValue() throws Exception{
stStack.push("test");
Ensure.that(stStack.pop(), m.is("test"));
}
为 Matcher 挑选 ‘M’
关于 UsingMatchers 范例
您大概留意到,清单 12 中的代码并不是很优雅。清单 11 中的 m 确实有点 影响代码的可读性(“ensure that pop’s value m (what the?) is test)。 可以通过扩展 JBehave 提供的一个专门的基类(UsingMiniMock)来制止利用 UsingMatchers 范例。这样一来,清单 11 中最后一行就酿成 Ensure.that (stStack.pop(), is("test")),这样可读性更好一点。
在清单 11 中,我确保 pop() 返回值 “test”。在利用 JBehave 的 Ensure 类的进程中,您经常会发明,需要一种更富厚的方法来表达期望。 JBehave 提供了一种 Matcher 范例用于实现富厚的期望,从而满意了这一需求 。而我选择重用 JBehave 的 UsingMatchers 范例(清单 11 中的 m 变量), 所以可以利用 is()、and()、or() 等要领和许多其它整洁的机制来构建更具文 学性的期望。
清单 11 中的 m 变量是 StackBehavior 类的一个静态成员,如清单 12 所 示。
清单 12. 行为类中的 UsingMatchers
private static final UsingMatchers m = new UsingMatchers(){};
有了清单 11 中编写的新的行为要领之后,此刻可以来运行它 — 可是这时 会发生一个错误,如清单 13 所示。
清单 13. 新编写的行为不能运行
Failures: 1.
1) StackBehavior should pop pushed value:
java.lang.RuntimeException: nothing to pop
怎么回事?本来是我的 push() 要领还没有落成。回到 清单 5,我编写了一 个最简朴的实现,以使我的行为可以运行。此刻是时候完成这项事情了,即真正 将被推入的值添加到内部容器中(假如这个值不为 null)。如清单 14 所示。
清单 14. 完成 push 要领
public void push(E value) {
if(value == null){
throw new RuntimeException("Can't push null");
}else{
this.list.add(value);
}
}
可是,等一下 — 当我从头运行该行为时,它仍然失败!
清单 15. JBehave 陈诉一个 null 值,而不是一个异常
1) StackBehavior should pop pushed value:
VerificationException: Expected:
same instance as <test>
but got:
null:
#p#分页标题#e#
至少清单 15 中的失败有别于清单 13 中的失败。在这种环境下,不是抛出 一个异常,而是没有发明 "test" 值;实际弹出的是 null。仔细调查 清单 10 会发明:一开始我将 pop() 要领编写为当内部容器中有项目时,就返回 null。 问题很容易修复。
清单 16. 是时候编写完这个 pop 要领了
public E pop() {
if(this.list.size() > 0){
return this.list.remove(this.list.size());
}else{
throw new RuntimeException("nothing to pop");
}
}
可是,假如此刻我从头运行该行为,我又收到一个新的错误。
清单 17. 另一个错误
1) StackBehavior should pop pushed value:
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
仔细阅读清单 17 中的实现可以发明问题:在处理惩罚 ArrayList 时,我需要考 虑 0。
清单 18. 通过思量 0 修复问题
public E pop() {
if(this.list.size() > 0){
return this.list.remove(this.list.size()-1);
}else{
throw new RuntimeException("Nothing to pop");
}
}
栈的逻辑
至此,通过答允通报多个行为要领,我已经实现了 push() 和 pop() 要领。 可是我还没有处理惩罚栈的实际内容,这是与多个 push() 和 pop() 相关联的逻辑 ,间或呈现一个 peek()。
首先,我将通过 shouldPopSecondPushedValueFirst() 行为确保栈的根基算 法(先进先出)无误。
清单 19. 确保典范的栈逻辑
public void shouldPopSecondPushedValueFirst() throws Exception{
stStack.push("test 1");
stStack.push("test 2");
Ensure.that(stStack.pop(), m.is("test 2"));
}
清单 19 中的代码可以按打算运行,所以我将实现另一个行为要领(在清单 20 中),以确保两次利用 pop() 都能表示出正确的行为。
清单 20. 更深入地查察栈行为
public void shouldPopValuesInReverseOrder() throws Exception{
stStack.push("test 1");
stStack.push("test 2");
Ensure.that(stStack.pop(), m.is("test 2"));
Ensure.that(stStack.pop(), m.is("test 1"));
}
接下来,我要确保 peek() 能按预期运行。正如 Linda 所说,peek() 遵从 和 pop() 沟通的法则,可是 “应该保存栈顶的项目”。相应地,我在清单 21 中实现了 shouldLeaveValueOnStackAfterPeep() 要领的行为。
清单 21. 确保 peek 保存栈顶的项目
public void shouldLeaveValueOnStackAfterPeep() throws Exception{
stStack.push("test 1");
stStack.push("test 2");
Ensure.that(stStack.peek(), m.is("test 2"));
Ensure.that(stStack.pop(), m.is("test 2"));
}
由于 peek() 还没有界说,因此清单 21 还不能编译。在清单 22 中,我定 义了 peek() 的一个最简朴的实现。
清单 22. 当前,peek 是必须的
public E peek() {
return null;
}
此刻 StackBehavior 类可以编译,可是它仍然不能运行。
清单 23. 返回 null 并不奇怪,对吗?
1) StackBehavior should leave value on stack after peep:
VerificationException: Expected:
same instance as <test 2>
but got:
null:
在逻辑上,peek() 不会从内部荟萃中移除 项目,它只是通报指向谁人项目 的指针。因此,我将对 ArrayList 利用 get() 要领,而不是 remove() 要领, 如清单 24 所示。
清单 24. 不要移除它
public E peek() {
return this.list.get(this.list.size()-1);
}
栈为空的环境
此刻从头运行 清单 21 中的行为,功效顺利通过。可是,在这样做的进程中 发明一个问题:假如栈为空,则 peek() 有奈何的行为?假如说栈为空时挪用 pop() 会抛出一个异常,那么 peek() 是否也应该如此?
Linda 对此没有举办表明,所以,显然我需要本身添加新的行为。在清单 25 中,我为 “当之前没有挪用 push() 时挪用 peek() 会奈何” 这个场景编写了 代码。
清单 25. 假如没有挪用 push 就挪用 peek,会奈何?
public void shouldReturnNullOnPeekWithoutPush() throws Exception{
Ensure.that(stStack.peek(), m.is(null));
}
同样,不会感想意外。如清单 26 所示,问题呈现了。
清单 26. 没有可执行的内容
1) StackBehavior should return null on peek without push:
java.lang.ArrayIndexOutOfBoundsException: -1
修复这个缺陷的逻辑雷同于 pop() 的逻辑,如清单 27 所示。
清单 27. 这个 peek() 需要做一些修复
#p#分页标题#e#
public E peek() {
if(this.list.size() > 0){
return this.list.get(this.list.size()-1);
}else{
return null;
}
}
把我对 Stack 类作出的所有修改和修复综合起来,可以获得清单 28 中的代 码。
清单 28. 一个可正常事情的栈
import java.util.ArrayList;
public class Stack<E> {
private ArrayList<E> list;
public Stack() {
this.list = new ArrayList<E>();
}
public void push(E value) {
if(value == null){
throw new RuntimeException("Can't push null");
}else{
this.list.add(value);
}
}
public E pop() {
if(this.list.size() > 0){
return this.list.remove(this.list.size()-1);
}else{
throw new RuntimeException("Nothing to pop");
}
}
public E peek() {
if(this.list.size() > 0){
return this.list.get(this.list.size()-1);
}else{
return null;
}
}
}
在此,StackBehavior 类运行 7 种行为,以确保 Stack 类能凭据 Linda 的 (和我本身的一点)类型运行。Stack 类 还大概利用某种重构(也许 pop() 方 法 应该挪用 peek() 举办测试,而不是执行 size() 查抄?),可是由于一直 利用了行为驱动进程,我可以很自信地对代码作出变动。假如呈现了问题,很快 就可以收到通知。
竣事语
您大概已经留意到,本月对行为驱动开拓(BDD)的摸索中,Linda 实际上就 是客户。在这里,可以把 Frank 看作开拓人员。假如把这里的规模(即数据结 构)换成其它规模(譬喻一个呼唤中心应用措施),以上应用仍然雷同。作为客 户或规模专家的 Linda 指出系统、特性或应用措施应该 执行什么成果,像 Frank 这样的开拓人员则利用 BDD 确保正确领略了她的要求并实现这些需求。
对付许多开拓人员来说,从测试驱动开拓转移到 BDD 是明智的转变。假如采 用 BDD,就不必思量测试,而只需留意应用措施的需求,并确保应用措施的行为 执行它 应该 执行的成果,以满意那些需求。
在这个例子中,利用 BDD 和 JBehave 使我可以按照 Linda 的说明轻松地实 现一个可正常事情的栈。通过首先 思量行为,我只需倾听她的需求,然后相应 地构建栈。在此进程中,我还发明白 Linda 没有提及的关于栈的其他内容。