TDD: Tricky Driven Development

 

命名

測試用例的名字應該描述需求, 不要描述實現.

取決於你要溝通交流傳遞的信息, Test Case 有至少兩個作用

  1. 檢查你的產品代碼是否按預期工作, 這由函數體來完成

  2. 表達你的預期,讓閱讀代碼的人知道你的產品能夠幹什麼,如何使用, 甚至如何設計的;這除了函數體的assert語句外,Test case的名字更是重要的手段

但我們通常只會爲一段測試代碼起一個名字, 而要表達的信息如此之多, 怎麼辦? 一個測試用例儘量只有一個斷言, 或至少限制在一類斷言, 這時候你就能夠起一個Domain相關的名字

如果Domain比較複雜, 進入項目的新人不一定了解, 這時候你的測試用例的名字就是最好的領域知識, 比如電信計費系統:

 

    public void testShouldBeFreeFrom2amTo5am() throws Exception {

        Duration talkingDuration = new Duration("2am", "5am");

        Money fee = chargeSystem.charge(talkingDuration);

        assertEquals(Money.ZERO, fee);

    }

 

Setup

所有測試相關的代碼, 包括設置測試環境, 調用被測對象觸發測試, 斷言測試結果等, 應該放在一起,便於閱讀和理解; 那setUp()裏放什麼?

  1. 如果一個對象很容易用一兩句話裝配, 一般可以在每個測試用例裏就近創建它

  2. 如果初始化一個對象很複雜, 要不少代碼, 就寫一個函數來做初始化, 然後在每個測試用例裏就近調用它

  3. setUp()裏放點公用的基礎的比如對外部依賴環境的設置

 

Mock stub

1, 真實數據/環境

隨着自動化單元測試, Mock技術的流行, 人們似乎逐漸的鄙視使用真實的環境和數據來測試; 但真實的環境和數據並不是天然就與自動化無緣, 只要它可以很容易的獲得, 並能夠伴隨測試代碼一起發佈

這方面典型例子是文件系統, 可以使用相對路徑消除對存放位置的依賴, 使用統一的入口如 TestFilesUtil 來負責對測試數據的訪問.

真實數據/環境的好處是它最接近生產環境, 可以通過提供不同的數據來產生不同的測試用例, 比較方便的提高測試覆蓋率

真實數據/環境的壞處就是應用範圍比較窄,雖然有"嵌入式"的數據庫和FTP服務器等,但很少在單元測試中使用;我們主要是在測試以文件作爲輸入的API的時候使用真實的文件系統

2, 靜態手寫Stub

這是初期比較常用的方法, 是State Based Test方法的實現方式 (與之對應的Interaction Based Test/ Behaviour verification Test的實現方式是Mock Object,   或者Classical TDD vs. Mocklist TDD, 反正就是這麼兩個意思

好處是簡單, 易於獲得, 符合既有的思維習慣, 壞處是繁瑣, 最終測試代碼中充斥着大量Stub類

Stub類的編寫應該遵循以下原則: 

  • 不要包含任何邏輯;

就是所有函數都簡單的return一個固定的結果;  一旦你的Stub包含邏輯, 你就需要爲你的Stub也寫一個測試了, 呵呵, 我們的系統中現在就有這麼一個Stub, 陪伴它的是它的測試用例

Stub 會帶來一個好處:

  • 強迫你重新考慮你的設計 (其實這幾乎是任何高質量的測試用例都能帶來的)

但Stub尤其會在兩個方面強迫你重新思考:

    1), 調用鏈

     就是當你的產品代碼中出現如下調用時  DateTime buildDate = obj.getConfigure().getProject().getBuild().getDate();

     你如何使用Stub來測試這段代碼呢? 很不幸, 你需要一鼓作氣寫三個Stub類分別代理Configure/Project/Build才能完成測試;  這就強迫你重新思考, 原來的設計是否有問題, 是否需要重構

    2), 針對接口編程

      OO完美主義者傾向於用構造函數建立起一個不變式, 所以經常在構造函數中進行各種計算和驗證, 一旦發現不符合不變式就會拋出異常之類; 這就給Stub帶來一個問題, 因爲要調用基類的構造函數, 傳遞什麼樣的參數才能不讓基類的構造函數拋出異常呢? 通常在測試環境中我們不容易滿足這類約束, 這就迫使我們抽取本來就應該抽取的接口, 將函數的參數類型或返回值類型替換爲該接口, 然後只要Stub實現這個接口即可

 

3, Mock Object

真實數據是測試世界的北極, Mock Object就是南極 

Mock Object的經驗不多, 所以首先感受到的是它的不便之處:  重構會破壞測試用例, 即使你的重構是正確的

與有的Mock Object實現對重構的"Rename"之類的操作支持不好相比, 重構直接破壞測試用例更鬱悶:

  1. 比如你用一個現有的充分測試過的第三方的API替換了你自己寫的幾行實現

  2. 比如你刪除了幾行冗餘的調用

  3. 甚至你把幾個public函數中重複邏輯抽取到一個私有函數裏, 都有可能破壞基於Mock Object的test case.

然而與Mock Object的誤用相比, 重構破壞測試用例便不算什麼了. 毫無疑問有些情況下是不應該使用Mock Object的, 這尤其體現在那些緊湊的API上, 即單一API調用, 根據不同參數返回不同結果; 這類API包括:

  1. 傳入一個xpath表達式, 返回NodeList

  2. 傳入正則表達式, 返回匹配結果

  3. 傳入SQL語句, 返回結果集

這類API的返回結果強烈依賴於它們的參數, 參數纔是它們的核心, 你一旦在你的產品代碼中使用了這些API, 又在測試用例中Mock了這些API, 直接返回固定的結果, 那麼恭喜你, 你的測試白做了. (這類API對Stub也不感冒, 儘可能用真實數據來測)

Mock Object的適用場景其實和Stub差不多, 首要目的是減少對系統其它部分包括外部系統的依賴. 但Mock對交互順序和參數/返回值傳遞強大的支持可以使你更精確的斷言你的代碼的行爲 考慮經典的用戶存款場景, 假設在此過程中, 你的API會進一步調用銀行API來完成操作, 如果使用State Based 測試方法, 我們的測試用例可能只是在調用你的API前後斷言一下用戶賬戶的balance就算了, 比如:

    public void testBalanceShouldIncrease50WhenDeposit50() throws Exception {

        double balanceBeforeDeposit = getBalance();

        deposit(50);

        double balanceAfterDeposit = getBalance();

        assertEquals(balanceBeforeDeposit + 50, balanceAfterDeposit);

    }

通常這樣的測試也算測過了, 但這樣我們無法測試錢是直接進了你的賬戶., 還是中間流轉了一下; 如果金融系統的客戶對自己內部的系統要求比較嚴格, 你可能需要對這中間的內部調用邏輯進行測試, 以避免洗錢之類的可能, 這時候你就可以用Mock來斷言銀行的API確實以參數50被調用了一次, 而不是以參數25調用了兩次

    public void testDepositShouldPutMoneyToYourAccountDirectly() throws Exception {

        depositMock.expect(once()).method("deposit").with(eq(50));

 

       double balanceBeforeDeposit = getBalance();

        deposit(50);

        double balanceAfterDeposit = getBalance();

        assertEquals(balanceBeforeDeposit + 50, balanceAfterDeposit);

    }

Mock Object 還支持交互順序的測試. 比如你有兩個操作, 必須以確定的順序來執行, 你擔心後續的維護者會破壞這種約定, 則可以使用 Mock 測試顯式的描述它.

關於 Mock 和 Stub 的其它描述, 請參考 <<敏捷質疑: TDD>>

 

測試殺手 

1.

static 

method,  

new

operator

這是一個Spring的時代了, 你還在用

static 

method 嗎? 還在用  

new

operator 去創建對象嗎?  僅僅"讓你的API難以測試"這一條便足以宣判他們的死刑了

記得很早以前就寫過: "

RAII讓我告別了delete,IoC讓我告別了

new

"

關於 static 

 

new 的測試, 請參考<<假冒的藝術>>

2. Prolonged failed test case

"小步前進"是確保TDD成功的衆多因素中的一條, 更早以前還寫過: "

目標驅動生活,每天早上運行一遍測試用例:assertTrue(有房);assertTrue(有車),測試失敗,就努力讓它早日通過

"

感謝當年提出"步子太大,這個測試fail的時間太長"意見的朋友, Blog的變遷使得這條評論已經不見了,  但真理亙古不變,我現在決定把它改成"assertTrue(每月咱也打回的); assertTrue(不要頓頓三明治)"

 

 

測試朋友

1, 誰是誰的測試,  

當你重構測試用例的時候?

當你重構產品代碼的時候, 測試用例是你的朋友, 可以確保重構不會引入錯誤; 

那麼當你的測試越來越多, 需要重構一下便於增加新的測試的時候,  誰是誰的測試呢?

 

2. IDE

我的設想是強迫你TDD的IDE;  現在的IDE, 如Eclipse, 它的新建Class的嚮導不會讓你選擇這個新類需要滿足的測試用例, 而它的新建測試用例嚮導會讓你選擇打算測試哪個類

新的支持TDD的IDE會反過來, 你不能憑空新建一個類, 除非它是測試用例類, 當你新建類的時候, 你必須指定它被用來滿足哪個失敗的Test Case

 

3. 評審員

直接從以前的Blog中搬過來:    " 測試一詞是個錯誤的用法,表達了一種實現,手段,過程,而不是目標、結果;一個副作用就是令測試人員不被重視;代碼複審和系統測試目的都是找出編碼中的錯誤,但一種叫評審員,一種叫測試人員;建議在目前出現測試一詞的地方,都替換爲“驗證”,如系統驗證,驗證人員,“xxx,那個bug我改了,你再幫我驗證一下”,呵呵,顛覆一把話語體系"

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章