[Book] 單元測試的藝術:Ch7. Trustworthy
在任何的測試中,如果沒有沒有三個要素:
- 可信賴的 (Trustworthy)
- 可維護的 (Maintainability)
- 可讀的 (Readability)
我們所撰寫的測試有無法有效地發揮其價值
可信賴的 : 如果我們不相信我們測試能確保我們功能無誤,撰寫測試的意義就沒了
可維護的 : 就算我們確保測試可以確保我們的功能沒問題,但是維護麻煩,會嚴重干擾開發進度,也會使我們放棄撰寫測試
可讀的 : 最重要的因素,如果沒有可讀性,就無法產生可信賴和可維護的特性
我們會從 可信賴的 的特性開始探討
什麼是“可信賴的” (Trustworthy)?
可信賴的核心就是
在該通過的時候通過,在該失敗的時候失敗
再說的明白一些,就是當
測試失敗時,你會不會說:
- 「喔,他本來就會失敗」
- 「這不代表產品程式有問題」
你會相信當測試失敗時,產品程式碼真的出了問題
當測試成功時,你不會說:
- 「我要再用 debugger 一步步來確認我的程式碼有沒有問題」
你會相信測試過的程式碼確實地完成了需求
可信任的測試讓我們對產品程式碼有良好的掌握度
撰寫可信任測試的原則
以下一些原則,可以幫我們撰寫可信賴的技術
- 決定何時刪除或修改測試
- 避免測試帶邏輯
- 一次只測試一個關注點
- 推行程式碼審查
決定何時刪除或修改測試
是否需要更改測試? | |
---|---|
產品 bug | ❌ |
測試 bug | ✅ |
語意或 API 變更 | ✅ |
矛盾或無效的測試 | ⚠️ (according to requirement) |
重新命名或重構測試 | ✅ |
刪除重複的測試 | ✅ (pros > cons) |
1. 語意或 API 變更
程式碼如下
/* 8.1 - original Api */
public void SemanticsChange() {
LogAnalyzer logan = new LogAnalyzer();
Assert.IsFalse(logan.IsValid("abc");
}
當 class LogAnalyzer
的使用方法有變更,需要初始化(使用 Initialize
method),
此時,我們就一定要更改測試
/* 8.2 - Api changed - need initialize */
public void SemanticsChange() {
LogAnalyzer logan = new LogAnalyzer();
logan.Initialize();
Assert.IsFalse(logan.IsValid("abc");
}
小技巧:
我們可以用工廠模式處理初始化的部分,讓 call
Initialize
不用寫在每個 test case 中,即遵守 DRY 原則
/* 8.3 - refactor by Factory pattern */
public void SemanticChange() {
LogAnalyzer logan = MakeDefaultAnalyzer();
Assert.IsFalse(logan.IsValid("abc");
}
public void MakeDefaultAnalyzer() {
LogAnalyzer anaylzer = new LogAnalyzer();
analyzer.Initialize();
return analyzer;
}
2. 矛盾或無效的測試
舊需求:允許長度 3 的檔名
新需求:只允許長度 ≥ 4 的檔名
if 舊需求 test pass → 新需求 test fail
vice versa
確認需求是否本身就矛盾,確認最後的需求,刪除另一個矛盾的
3. 重新命名或重構測試
當測試的可讀性低時,就需要重新撰寫測試
4. 刪除重複的測試
刪除重複的測試各有利弊,如下:
Pros
- 測試越多,越可能發現 bug
- 同一個測試,可以看到不同設計方法和語意
Cons
- 一個問題會導致多個失敗
- 相似的測試必須用不同的名字,否則會分散在不同的類別中
- 多個測試會帶來維護性的問題
作者的建議是:刪除重複的測試,即使他沒那麼熱衷
因為重複的測試帶來的壞處還是大於好處, 主要原因是影響到後續的可維護性
避免測試帶著邏輯
邏輯:如何正確地達成我們想要的需求,重點在如何達成
但在測試中,我們不關心如何達成,我們只在意結果
案例:隨機產生亂數當作數入 → 測試結果不穩定 → 不可信任
不應該有:
switch
、if else
foreach
、for
、while
Cons:
- 難以閱讀和理解
- 測試難以重現
- 測試比較容易發生 bug 或測試了錯誤的事情
- 難以命名測試,因為他執行的多個任務
我們測試 user + greeting 組成的句子
public void ProductionLogicProgram() {
string user = "USER";
string greeting = "GREETING";
string actual = MessageBuilder.Build(user, greeting);
Assert.AreEqual(user + greeting, actual);
}
實際上,我們需要 user 和 greeting 中間有一個空格,
上述用邏輯撰寫會更容易出錯,因此寫死固定值會更好
public void ProductionLogicProgram() {
string user = "USER";
string greeting = "GREETING";
string actual = MessageBuilder.Build(user, greeting);
Assert.AreEqual("user greeting", actual);
}
一次只測試一個關注點
一個關注點是工作單元的最終結果,可能是:
- return value
- state changed
- 3rd party interaction
測試多個關注點的缺點:
- 命名困難
- 如果第一個測試拋出錯誤沒有被抓住,我們只看得見第一個測試失敗
範例如下
public void IsValid_WhenValid_ReturnsTrueAndRemembersItLater() {
LogAnalyzer logan = MakeDefaultAnalyzer();
Assert.IsTrue(logan.IsValid("abc"));
// 2nd assert will not be called !!
Assert.IsTrue(logan.WasLastCalledValid);
}
expected result 的命名不好:
- 名稱冗長
- 若要簡化名稱,就會太過抽象,導致可讀性差
可能只顯示第一個錯誤:
- 若一個個錯誤拋出了例外 & 沒有被捕捉,我們無法閱讀到第二個錯誤,
那我們就會誤以為只有第一個錯誤產生,而只解決第一個錯誤
但如果我們可以同時看見所有的錯誤,我們有機會發現更根本的問題
- 若一個個錯誤拋出了例外 & 沒有被捕捉,我們無法閱讀到第二個錯誤,
我們應該拆成多個單元測試,每次只測試一個關注點 (結果)
推行程式碼審查
- 幫助每個工程師掌握全局
- 增加學習的機會
- 容易產生易讀、高品質、耐用的程式碼
最後,我們可以利用下列清單建立可信任測試的步驟
- 1 - 註解掉你認為測試所涵蓋的測試碼
- 2 - 執行所有測試
- 3 - 如果全過,表示少一個測試 or 測試了錯誤的東西
- 4 - 補上測試碼,並持續註解產品程式碼,新的測試碼預期要是失敗的
- 5 - 移除註解
- 6 - 新的測試碼應該要通過,表你已經成功加入一個可信賴的測試
- 7 - 如果新測試失敗,說明測試有 bug