开发人员测试显然是一件好事。 尽早进行测试(例如,在编写代码时)是一件更好的事情,尤其是在代码质量方面。 首先编写测试,您将赢得最高荣誉。 不可否认的是,能够检查代码行为并进行抢先调试的额外动力无疑是高速的。
即使知道这一点,我们仍然远没有达到在编写代码成为标准惯例之前编写测试的临界质量。 正如TDD是极限编程(Extreme Programming)的演进式下一步一样,极限编程将单元测试框架推到了风口浪尖,而TDD的基础正等待着进化的飞跃。 本月,我邀请您加入我的行列,实现从TDD到其更直观的表亲(行为驱动开发(BDD))的进化飞跃。
行为驱动的发展
尽管测试优先编程对某些人有效,但并非对所有人都适用。 对于每个热衷于TDD的应用程序开发人员,还有许多其他人积极地反对它。 即使有诸如TestNG,Selenium和FEST之类的大量测试框架, 也不进行代码测试的原因也是多种多样的。
不执行TDD的两个常见原因是“没有足够的时间进行测试”和“代码过于复杂且难以测试”。 测试优先编程的另一个障碍是测试优先概念本身。 我们许多人将测试视为一种触觉活动,比抽象更具体。 经验告诉我们,不可能测试尚不存在的东西。 对于某些开发人员而言,鉴于此概念框架, 首先进行测试的想法是矛盾的。
但是,如果您不是在思考编写测试以及如何测试事物的方式,而是在思考行为 ,该怎么办? 通过行为,我指的是应用程序应如何行为-本质上是其规范。
事实证明,您已经这样认为。 大家都这样做。 看。
弗兰克:什么是堆栈?
Linda:这是一种数据结构,以先进先出(或后进先出)的方式收集对象。 它通常具有一个带有push()
和pop()
类的方法的API。 有时,您也会看到peek()
方法。
弗兰克:push()
什么作用?
Linda:push()
接受一个输入对象,例如foo
,并将其放入内部容器(如数组)中。push()
通常也不返回任何内容。
弗兰克:如果我push()
两件事,比如foo
然后是bar
怎么办?
琳达:第二个对象bar
应该位于概念堆栈的顶部(至少包含两个对象),因此,如果调用pop()
,则bar
应该移开而不是第一个对象(在您的情况下是foo
。 如果再次调用pop()
,则应该返回foo
,并且堆栈应该为空(假设在添加两个对象之前其中没有任何内容)。
弗兰克:那么pop
会删除放入堆栈中的最新项目吗?
琳达:是的,pop()
应该删除顶部的项目(假设有要删除的项目)。peek()
遵循相同的规则,但未删除该对象。peek()
应该将顶部项目保留在堆栈中。
弗兰克:如果我在没有推送任何内容的情况下调用pop()
怎么办?
琳达:pop()
应该抛出一个异常,表明尚未推送任何内容。
弗兰克:如果我push()
null
怎么办?
Linda:堆栈应该引发异常,因为null
不是push()
的有效值。
注意到此对话有什么特别之处吗(除了Frank并不主修计算机科学的事实)? “测试”一词在任何地方都没有使用过。 但是,“应该”一词很自然地在这里和那里溜走了。
做自然而然的事情
BDD并不是什么新鲜事物或革命性的东西。 这只是TDD的进化分支,其中“ test”一词被替换为“ should”。 除了语义,许多人发现应该比测试的概念更自然地推动开发。 考虑行为(应该)以某种方式为首先编写规范类铺平了道路,而规范类又可以成为非常有效的实现驱动程序。
以Frank和Linda之间的对话为基础,让我们看一下BDD如何以TDD旨在促进的方式推动发展。
杰贝夫
JBehave是受xUnit范例启发的Java™平台的BDD框架。 您可能会猜到,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);
}
});
}
很高的期望和超越
清单3中发生了一些JBehave特有的事情,所以让我解释一下。 首先,我创建Stack
类的实例并将其限制为String
类型(通过Java 5泛型)。 接下来,我使用JBehave的Expectation 框架对所需的行为进行建模。 Ensure
类类似于JUnit或TestNG的Assert
类型。 但是,它添加了一系列促进API更具可读性的方法(通常称为识字编程 )。 在清单3中,我确保了如果调用带有null
push()
,则将引发RuntimeException
。
JBehave还引入了一个Block
类型,该类型通过用所需的行为覆盖run()
方法来实现。 在内部,JBehave确保不会引发(并因此捕获)您所需的异常类型,从而生成故障状态。
如果现在运行清单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报告它在运行所需行为时没有得到任何结果。 当然,我的下一步是使行为通过,这是通过检查清单5中的null
来完成的。
清单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()
支持固定装置。 鉴于我可能会在整个行为类中使用一个Stack
实例,所以我最好将逻辑推入(没有双关语),如清单7所示。注意,JBehave将遵循相同的步骤JUnit遵循的Fixture契约-也就是说,它将为每个行为方法运行setUp()
和tearDown()
。
清单7. JBehave中的装置
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中的行为时,事情将按预期工作:由于堆栈未保存任何值(因此,其大小不大于零),将引发异常。
下一个行为方法称为shouldPopPushedValue()
,结果方法很容易指定。 我只是简单地push()
一个值( "test"
),并确保当我调用pop()
,返回相同的值。
清单11.如果推送一个值,它应该弹出,对吗?
public void shouldPopPushedValue() throws Exception{
stStack.push("test");
Ensure.that(stStack.pop(), m.is("test"));
}
拨打“ 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报告一个空值而不是一个异常
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()
es和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.确保偷看将最上面的项目留在堆栈上
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()
是否应该这样做?
琳达对此没说什么,所以显然,我需要自己充实一些新的行为。 在清单25中,我将场景编码为“如果在没有push()
情况下调用peek()
会发生什么”。
清单25.如果在没有推送的情况下调用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
如清单27所示,修复缺陷的逻辑与pop()
的逻辑非常相似。
清单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
类将运行七个行为,以确保Stack
类根据Linda的规范(以及我自己的一些规范)工作。 Stack
类可能会使用一些重构(也许pop()
方法应该调用peek()
作为测试,而不是size()
检查?),但是由于到目前为止我的行为驱动过程,我有了基础架构做出几乎完全置信的变化。 如果我弄坏了东西,我会很快得到通知。
结论
您可能已经注意到,本月对行为驱动开发或BDD的探索是,Linda本质上是客户。 您可能认为Frank是这种情况下的开发人员。 删除该域(在这种情况下为数据结构),然后用其他内容(例如,呼叫中心应用程序)替换该域,此过程类似。 客户或领域专家Linda表示系统,功能或应用程序应该做什么,而Frank之类的人则使用BDD来确保他正确地听到了她的声音并实现了她的要求。
对于许多开发人员来说,从测试驱动开发到BDD的转变是明智之举。 随着BDD,你不必去想的测试,你可以只注意你的应用程序的要求,并确保应用程序的行为做什么它应该以满足这些要求。
在这种情况下,使用BDD和JBehave使我可以轻松实现基于Linda规范的工作堆栈。 我只是听她在说什么,然后据此构建堆栈, 首先在行为的角度来思考。 在此过程中,我还设法发现了Linda关于堆栈的一些遗忘。
翻译自: https://www.ibm.com/developerworks/java/library/j-cq09187/index.html