TDD: Tricky Driven Development

來源:互聯網
上載者:User

 

命名

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

取決於你要溝通交流傳遞的資訊, 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 stub1, 真實資料/環境

隨著自動化單元測試, 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. 傳入Regex, 返回匹配結果

  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我改了,你再幫我驗證一下”,呵呵,顛覆一把話語體系"

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.