程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 追求代碼質量 - 親身體驗行為驅動開發

追求代碼質量 - 親身體驗行為驅動開發

編輯:關於JAVA

顯然,測試本身是件好事。而在早期進行測試 — 例如在編寫代碼時 — 則 更有益處,這特別有利於提高代碼質量。在開發早期編寫測試,您將獲益良多。 您能夠檢查代碼的行為,並預先對它進行調試,這種動力無疑是巨大的。

即使了解了這種重要性,我們也沒有達到關鍵的一點:使在編寫代碼之前 編 寫測試成為一種標准實踐。正如 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 希望推廣的方式驅動開發。

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{} 
} 

清單 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>();
  }
  //...
}

對於接下來的行為方法,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:

至少清單 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 沒有提及的關於棧的其他內容。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved