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

老於`發表於2021-01-28

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

  1. 可靠性****。開發人員希望執行的測試可靠,能夠對測試結果有信心。可靠的測試沒有缺陷而且測試正確的事情
  2. 可維護。性無法維護的測試是夢,它們會拖延專案計劃,或者當專案日程緊張時被擱置一旁。如果修改測試花費時間過多,或者經常需要為很小的產品程式碼頻繁變更修改測試,開發人員會直接停止測試的維護和修復工作
  3. 可讀性。人們不僅要能夠閱讀測試,還要在測試出問題時找出癥結所在。失去可讀性另外兩個支柱很快也會倒塌。如果無法理解測試,測試的維護工作就會變得困難,也無法得到人們的信任。

1. 可靠性

1.1 及時維護測試程式碼

測試程式碼與產品程式碼一樣需要不斷進行維護,一旦測試寫好了並且通過了,通常是不應該修改或刪除這些測試的。這些測試是你的保護網告訴你修改的程式碼是否破壞了已有的功能。雖說如此,有時可能還是需要修改或者刪除已有的測試。要理解什麼情況下修改或刪除測試會帶來問題,什麼情況下這麼做是合理的。
刪除一個測試的主要的理由是這個測試失敗了。如果一個測試突然開始失敗,可能有如下原因

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

如果測試或程式碼沒有任何問題,修改或刪除測試的原因有:

  • 重新命名或者重構測試

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

  • 去除重複程式碼

1.2 避免測試程式碼中的邏輯

如果單元測試中有下列任何一種語句,你的測試就包含了不應該有的邏輯:*

  • switch、if或e1se語句;*
  • foreach、for或whi1e迴圈。

包含邏輯的測試通常會一次測試多個東西,我們不推薦這種做法,因為這樣的測試可讀性較也比較脆弱。而且測試邏輯也增加了程式碼複雜度,可能包含隱藏的缺陷通常來說,一個單元測試應該是一系列的方法呼叫和斷言,但是不包含控制流語句,甚至不應將斷言語句包含在try- catch中。任何更復雜的語句都可能導致如下問題。

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

1.3 只測一個關注點

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

1.4 單元測試與整合測試分離

把整合混在單元測試裡放在測試專案中會導致很多方面的問題。這種測試難以執行,會讓人們誤以為程式碼有問題,浪費時間和精力進行檢查,最後導致開發人員不再信任這組測試。混在單元測試裡的整合測試就像筐裡的爛蘋果連累了其他的測試。如果下一次再發生類似的事情,開發人員甚至都不會去調查失敗原因,直接就說:“哦,那個測試有時候就是會失敗,沒事的。”要避免這樣的事情發生,就要建一個綠色安全區把整合測試和單元測試分開。
綠色安全區裡只包含單元測試。執行綠色安全區裡的所有測試測試結果應該全部是綠色的,如果有測試失敗,就說明出現了真正的程式碼問題,而不是因為某些配置或外部依賴倒置的假警報。

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)

相關文章