BDD行為驅動開發的介紹
阿新 • • 發佈:2018-12-24
行為驅動開發
行為驅動開發(Behaviour-Driven-Development)簡寫BDD
BDD是TDD的一種演化,作為一種設計方法,可以有效的改善設計,並在系統演化過程中未團隊知名前進方向
行為驅動開發的根基是一種“通用語言”。這種通用語言同時被客戶和開發者用來定義系統的行為。由於客戶
和開發者使用同一種語言來描述同一個系統, 可以最大程度避免表達不一致帶來的問題。
書寫格式:
Story:標題(秒速故事的單行文字)
As a 角色
I want 特徵
So that 利益
(用一系列的場景來定義驗證標準)
Scenario 1 :標題(秒速場景的單行文字)
Give [上下文】
And [更多上下文】
When [事件】
Then [結果】
清單 2. 用於探索行為的一個簡單的棧定義
值呼叫 push(),那麼棧應該 丟擲一個異常。現在看看我在清單 3 中如何定義這個行為。
清單 3. 如果推出一個 null 值,則棧應該丟擲一個異常
在清單 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. 沒有發生期望的行為
清單 4 中的句子 “StackBehavior should throw exception upon null push” 模擬行為的名稱(shouldThrowExceptionUponNullPush()),並加上類的名稱。 實際上,JBehave 是在報告當它執行所需的行為時,沒有獲得任何反應。當然,我的下一步是要使上述行為成功執行,為此我檢查 null,如清單 5 所示。
清單 5. 在棧類中增加指定的行為
當我重新執行行為時,一切都執行得很好,如清單 6 所示。
清單 6 中的輸出與 JUnit 的輸出是不是很像?這也許不是巧合,對不對?如前所述,JBehave 是根據 xUnit 範例建模的,它甚至通過 setUp() 和tearDown() 提供了對 fixture 的支援。由於我可能在整個行為類中使用一個 Stack 例項,我可能也會將那種邏輯推入(這裡並非有意使用雙關語)到一個 fixture 中,正如清單 7 中那樣。注意, JBehave 將與 JUnit 一樣遵循相同的 fixture 規則 — 也就是說,對於每個行為方法,它都執行一個 setUp() 和 tearDown()。
清單 7. JBehave 中的 fixture
清單 8. 確保 pop 的行為
確保行為
從技術上講,在這裡我可以將 pop() 實現為無論呼叫順序如何,都只丟擲一個異常。但是當我沿著這條行為路線前進時,我又忍不住考慮一個支援我所需要的規範的實現。在這種情況下,如果 push() 沒有被呼叫(或者從邏輯上講,棧為空)的情況下確保 pop() 丟擲一個異常,則意味著棧有一個狀態。正如之前 Linda 思考的那樣,棧通常有一個 “內部容器”,用於實際持有專案。相應地,我可以為 Stack 類建立一個ArrayList,用於保持傳遞給 push() 方法的值,如清單 9 所示。
清單 9. 棧需要一種內部的方式來持有物件
現在我可以為 pop() 方法編寫行為,即確保當棧在邏輯上為空時,丟擲一個異常。
清單 10. pop 的實現變得更容易
接下來的行為方法是 shouldPopPushedValue(),這個行為方法很容易指定。我只是 push() 一個值(“test”),並確保當呼叫 pop() 時,返回相同的值。
清單 11. 如果將一個值入棧,那麼出棧的也應該是它,對嗎?
為 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 方法
清單 15. JBehave 報告一個 null 值,而不是一個異常
清單 16. 是時候編寫完這個 pop 方法了
1) StackBehavior should pop pushed value:
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
仔細閱讀清單 17 中的實現可以發現問題:在處理 ArrayList 時,我需要考慮 0。
清單 18. 通過考慮 0 修復問題
棧的邏輯
至此,通過允許傳遞多個行為方法,我已經實現了 push() 和 pop() 方法。但是我還沒有處理棧的實際內容,這是與多個 push() 和 pop() 相關聯的邏輯,間或出現一個 peek()。
首先,我將通過 shouldPopSecondPushedValueFirst() 行為確保棧的基本演算法(先進先出)無誤。
清單 19. 確保典型的棧邏輯
清單 19 中的程式碼可以按計劃執行,所以我將實現另一個行為方法(在清單 20 中),以確保兩次使用 pop() 都能表現出正確的行為。
清單 20. 更深入地檢視棧行為
由於 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() 需要做一些修復
把我對 Stack 類作出的所有修改和修復綜合起來,可以得到清單 28 中的程式碼。
在此,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 分享了這方面的專業知識。
行為驅動開發(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.用於棧的一個簡單行為類
清單 1中定義的方法都是以should開頭,他們都建立一個類可讀的句子,這裡產生的StackBehavior類描述棧的特徵。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{} }
清單 2. 用於探索行為的一個簡單的棧定義
public class Stack<E> {
public void push(E value) {}
}
可以看到,我編寫了一個最簡單的棧,以便首先 新增必需的行為。正如 Linda 所說,行為很簡單:如果有人對 null值呼叫 push(),那麼棧應該 丟擲一個異常。現在看看我在清單 3 中如何定義這個行為。
清單 3. 如果推出一個 null 值,則棧應該丟擲一個異常
傑出的 expectation 和 overridepublic 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 中發生的一些事情是 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 分享了這方面的專業知識。