如何編寫優秀的測試代碼|單元測試

不管如何組織測試,不管有多少測試,若是你不能信任、維護以及閱讀它們,這些測試就幾乎沒有價值。要成爲優秀的測試,它們應該同時具備以下三個屬性。安全

  1. 可靠性****。開發人員但願運行的測試可靠,可以對測試結果有信心。可靠的測試沒有缺陷並且測試正確的事情
  2. 可維護。性沒法維護的測試是夢,它們會拖延項目計劃,或者當項目日程緊張時被擱置一旁。若是修改測試花費時間過多,或者常常須要爲很小的產品代碼頻繁變動修改測試,開發人員會直接中止測試的維護和修復工做
  3. 可讀性。人們不只要可以閱讀測試,還要在測試出問題時找出癥結所在。失去可讀性另外兩個支柱很快也會倒塌。若是沒法理解測試,測試的維護工做就會變得困難,也沒法獲得人們的信任。

1. 可靠性

1.1 及時維護測試代碼

測試代碼與產品代碼同樣須要不斷進行維護,一旦測試寫好了而且經過了,一般是不該該修改或刪除這些測試的。這些測試是你的保護網告訴你修改的代碼是否破壞了已有的功能。雖然說如此,有時可能仍是須要修改或者刪除已有的測試。要理解什麼狀況下修改或刪除測試會帶來問題,什麼狀況下這麼作是合理的。
刪除一個測試的主要的理由是這個測試失敗了。若是一個測試忽然開始失敗,可能有以下緣由多線程

  • 產品缺陷    被測試的產品代碼有缺陷。
  • 測試缺陷    測試中有缺陷。
  • 語義或者AP變動    被測試代碼的語義發生變化,可是功能不變
  • 衝突或者無效的測試    和測試相關的產品需求發生變化,產品代碼隨之變動

若是測試或代碼沒有任何問題,修改或刪除測試的緣由有:函數

  • 重命名或者重構測試

不可讀的測試帶來的麻煩比解決的問題更多。它會影響代碼的可讀性,妨礙你理解測試發現的問題
若是你看到測試名含義不清或者使人誤解,或者測試的可維護性有待提升,就應該修改測試代碼(可是不要改變測試的基本功能)單元測試

  • 去除重複代碼

1.2 避免測試代碼中的邏輯

若是單元測試中有下列任何一種語句,你的測試就包含了不該該有的邏輯:*學習

  • switch、if或e1se語句;*
  • foreach、for或whi1e循環。

包含邏輯的測試一般會一次測試多個東西,咱們不推薦這種作法,由於這樣的測試可讀性較也比較脆弱。並且測試邏輯也增長了代碼複雜度,可能包含隱藏的缺陷一般來講,一個單元測試應該是一系列的方法調用和斷言,可是不包含控制流語句,甚至不該將斷言語句包含在try- catch中。任何更復雜的語句均可能致使以下問題。測試

  • 測試難以閱讀和理解
  • 測試難以重現。(設想一下,若是一個多線程測試或者使用隨機數的測試忽然失敗了,該如何處理。)
  • 測試較容易包含缺陷或者測試錯誤的事情
  • 難以命名測試,由於它執行多件任務

1.3 只測一個關注點

如前所述,一個關注點是一個工做單元的一個最終結果:一個返回值、系統狀態的一個改變或者對第三方對象的一個調用。例如:若是你的單元測試對多個對象進行了斷言,那麼這個測試有可能測試了多個關注點。另外一種狀況是,它既測試了一個對象返回正確的值,又驗證系統狀態改變致使這個對象的行爲發生變化,那麼這個測試也可能測試了多個關注點。
測試多個關注點聽起來沒什麼,可是等到你要命名測試,或者考慮第一個對象的斷言失敗該如何處理時,就會遇到問題。
命名測試看似簡單,可是若是同時測試了多個東西,就幾乎不可能給測試起一個能說明測試內容的好名字。你最後起的名字可能很是通用,使得讀者不得不去閱讀測試代碼(本章的可讀性節詳細對此進行討論)。若是一次只測試一個關注點,測試命名就很簡單線程

1.4 單元測試與集成測試分離

把集成混在單元測試裏放在測試項目中會致使不少方面的問題。這種測試難以運行,會讓人們誤覺得代碼有問題,浪費時間和精力進行檢查,最後致使開發人員再也不信任這組測試。混在單元測試裏的集成測試就像筐裏的爛蘋果連累了其餘的測試。若是下一次再發生相似的事情,開發人員甚至都不會去調查失敗緣由,直接就說:「哦,那個測試有時候就是會失敗,沒事的。」要避免這樣的事情發生,就要建一個綠色安全區把集成測試和單元測試分開。
綠色安全區裏只包含單元測試。運行綠色安全區裏的全部測試測試結果應該所有是綠色的,若是有測試失敗,就說明出現了真正的代碼問題,而不是由於某些配置或外部依賴倒置的假警報。code

1.5 代碼審查與覆蓋率結合

代碼覆蓋率100%說明什麼呢?若是沒有作代碼審查,這個覆蓋率不能說明什麼。你的團隊可能會要求全部人的測試「達到95%以上的代碼覆蓋率」,你們可能也確實作到了。可是也許這些測試連斷言都沒有。人們一般會選擇作最少的事情達到某個指定的目標。
那麼代碼覆蓋率100%再加上測試和代碼審查能說明什麼呢?這說明整個世界都是你的。若是你作了代碼審查和測試審查,確保測試優秀並且覆蓋了全部代碼,那麼你就擁有了一個安全網,能夠避免愚蠢的錯誤,同時團隊也得到了分享的知識,從持續的學習中獲益對象

2. 可維護性

2.1 去除重複(Extract Method)

做爲開發者,單元測試中的重複代碼和產品代碼中的重複同樣(若是不是更加)有害。DRY原則應該一樣適用於測試代碼。重複代碼意味測試對象某方面改變時要修改更多的測試代碼。若是測試中有大量重複代碼,構造函數變動或者使用類的語義變化會產生極大的影響內存

2.2 測試隔離

測試隔離的基本概念是:一個測試應該老是在它本身的小世界中運行,與其餘進行相似或不一樣的工做的測試隔離甚至不知道其餘測試的存在。
若是沒有很好地隔離測試,它們會互相影響,使你很是悲慘,後悔在項目中嘗試單元測試決心之後不再作單元測試了。我見過這種狀況。開發人員不肯費心檢查測試中的問題,所以當出現問題時,須要花不少時間才能找到緣由有些測試一樣存在着一些壞味道可以提示測試隔離可能有問題。

  • 強制的測試順序    測試須要以某種特定順序執行,或者須要來自其餘測試結果的信息
  • 隱藏的測試調用    測試調用其餘測試。
  • 共享狀態損壞    測試共享內存裏的狀態,卻沒有回滾狀態。
  • 外部共享狀態損壞    集成測試共享資源,卻沒有回滾資源。

2.3 避免對不一樣的關注點屢次斷言(使用參數化測試)

Assert.AreEqual(2,Sum(1,2));
Assert.AreEqual(5,Sum(2,2));
Assert.AreEqual(6,Sum(5,2));

如上示例,這個測試方法中使用了三個斷言,進行了三個測試。這樣看起來在實際過程當中會節省一些寫代碼的時間,但會有一些問題。若是第一個斷言失敗,則後續斷言就不會在執行。而在這個示例中咱們是進行了三個測試。第一個斷言失敗就會致使咱們沒法得知另外兩個測試的測試結果。對於這種狀況咱們能夠採起別的方式進行測試

  1. 給每一個斷言建立一個單獨的測試
  2. 使用參數化測試
  3. 把斷言代碼放在一個try-catch塊中

2.4 避免過分指定

過分指定的測試對一個具體的被測試單元如何實現其內部行爲進行了假設,而不是隻檢查其最終行爲的正確性單元測試中過分指定主要有如下幾種狀況

  • 測試對一個被測試對象的純內部狀態進行了斷言
  • 測試使用多個模擬對象
  • 測試在須要存根時使用模擬對象
  • 測試在沒必要要的狀況下指定順序或使用了精確匹配。

3. 可讀性

不可讀的測試幾乎沒有任何意義。可讀性這條線鏈接着編寫測試的人和幾個月後閱讀測試的人。測試是你向項目的下一代開發者講述的故事,幫助開發者理解一個應用程序的組成及其開端。
測試可讀性有以下幾個方面

  • 命名單元測試
  • 命名變量
  • 使用好的斷言信息
  • 把斷言和操做分離

3.1 單元測試命名

命名標準很是重要,提供了合理的規則和模板,列出應該包括的測試信息。測試名通常包括三部分。

  • **被測試方法名    **很是關鍵,指明瞭被測試邏輯的位置。把被測試方法名放在測試方法開頭,能夠很容易地在測試類中瀏覽測試和使用智能感知(若是IDE支持)
  • 測試場景     說明了測試使用的條件:「若是我用一個nu11值調用方法x,那麼它應該執行Y。」
  • 預期行爲    基於當前場景,方法應該產生的行爲結果或者返回值,或者行爲方式:「若是用一個null值調用方法X,那麼它應該執行Y。」若是測試名缺乏上面列出的任何一部分,測試的讀者就會疑惑測試究竟在作什麼,須要閱讀測試代碼。合理地命名測試,主要目的就是爲了使後來的開發者從爲了理解測試而閱讀代碼的負擔中解脫出來。
public void IsValidFileName(){
	...
}

[Test]
public void IsValidFileName_WhenPNG_ReturnFalse(){
	...
}

如上示例,經過測試的方法命名咱們就能夠大概知道要測試的是方法是IsValidFileName當輸入參數是PNG的時候,預期返回False。

固然,你的團隊也能夠有適合本身的命名方式,但重要的是若是一個團隊中都有統一的有意義命名規範,那麼單元測試的可讀性將大大提高,而且有利於後來者快速進入項目,理解測試。

3.2 變量命名

測試中的變量命名和產品代碼中的命名規範一樣重要,經過合理的變量命名,咱們能夠確保閱讀測試的人能夠儘快的理解你要驗證什麼。

// 反例
Assert.AreEqual(100,actual);

如上示例,咱們常常會看到測試中出現"100"這樣的魔法數字。由於測試中沒有描述性的名字,也許你在剛剛寫完的時候還知道它是什麼意思,可是一週後,一月後,一年後呢?甚至你將來的繼任者看到這樣的測試代碼也是一頭霧水。

3.4 斷言和操做分離

不少人爲了「偷懶」常常會把斷言和方法調用卸載同一行裏,但這是一個很很差的習慣,它會大大下降代碼的可讀性。

// 反例
    Assert.AreEqual(true,fileManger.IsValidName())

// 正例
    bool expect=true;
    bool actual=fileManger.IsValidName();
    Assert.AreEqual(expect,actual)
相關文章
相關標籤/搜索