BDD行为驱动开发的介绍

行为驱动开发

行为驱动开发(Behaviour-Driven-Development)简写BDD
BDD是TDD的一种演化,作为一种设计方法,可以有效的改善设计,并在系统演化过程中未团队知名前进方向
行为驱动开发的根基是一种“通用语言”。这种通用语言同时被客户和开发者用来定义系统的行为。由于客户
和开发者使用同一种语言来描述同一个系统, 可以最大程度避免表达不一致带来的问题。
书写格式:
Story:标题(秒速故事的单行文字)
As a 角色
I want 特征
So that 利益
(用一系列的场景来定义验证标准)
Scenario 1 :标题(秒速场景的单行文字)
Give [上下文】
And [更多上下文】
When [事件】
Then [结果】

And [其他结果】


JBehave
JBehave是用于java平台的一个BDD框架,源于xUnit范例,JBehave强调“应该”这个词,而不是测试,和JUnit一样,
你可以在自己喜欢的IDE中,或者偏爱的构建平台(例如Ant)运行JBehave类

JBehave允许以JUnit的方式创建行为类,但是,在JBehave中,不需要扩展任何特定的基类,并且所有行为方法都需
要以should而不是test开发

清单 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中定义的方法都是以should开头,他们都创建一个类可读的句子,这里产生的StackBehavior类描述栈的特征。
清单 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>();
 }
 //...
}
对于接下来的行为方法,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’

在清单 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:
至少清单 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() 需要做一些修复

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 没有提及的关于栈的其他内容。

参考资料

学习

您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
“追求代码质量:对 Ajax 应用程序进行单元测试”(Andrew Glover,developerWorks,2007 年 7 月):通过使用 GWT 和它的重写类 Timer,测试 Ajax 应用程序变得更容易。
“追求代码质量: 使用 Selenium 和 TestNG 进行编程式测试”(Andrew Glover, developerWorks,2007 年 4 月):学习如何使用 TestNG 作为测试驱动器,通过编程的方式运行 Selenium 测试。
“使用 RSpec 进行行为驱动测试”(Bruce Tate,developerWorks,2007 年 8 月):在过去一年里,测试领域中最为瞩目的创新应属 RSpec 的引入和快速发展,它是一种行为驱动测试工具。了解 RSpec 如何改变人们思考测试的方式。
“Introducing BDD”(Dan North, DanNorth.net,2006 年 9 月):了解 Dan North 如何将 BDD 作为一种实践。
“Using BDD to drive development”(Andrew Glover,testearly.com,2007 年 7 月):Andrew 再次介绍 BDD 如何驱动开发,同样也是基于 JBehave。
“Mocks are hip when it comes to BDD”(Andrew Glover,thediscoblog.com,2007 年 7 月):Andrew 通过 JBehave 的 mocking 库重新发现 mock 对象,然后他使用这种对象驱动快速开发。
追求代码质量系列(Andrew Glover,developerWorks):学习更多关于编写专注于质量的代码的信息。
developerWorks Java 技术专区:这里有数百篇关于 Java 编程方方面面的文章。
获得产品和技术

下载 JBehave:面向 Java 平台的完全启用的BDD框架。
讨论

参与论坛讨论。
Discussion forum: Improve your code quality: 向代码质量完美主义者学习!作为一名专注于提高代码质量的顾问,Andrew Glover 分享了这方面的专业知识。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值