1. 程式人生 > >BDD行為驅動開發的介紹

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 分享了這方面的專業知識。