單元測試有助於避免尷尬、耗時的錯誤,將測試作為安全網只是一部分,更大部分是將測試表達為代碼的思考過程。
接下來的內容提煉自《單元測試的藝術(第2版)》和《有效的單元測試》兩本書。
一、質疑和回答
在組內推廣時,進度並不理想,遇到的阻礙大致可歸納為以下這幾種情況:
- 首先就是團隊成員會質疑單元測試的價值,需要給出證明單元測試確實有效可行的方法和證據。
- 其次是團隊成員缺乏主動測試的意識,目前有大量的測試代碼,不知道從哪里開始測試,並且花費額外的精力來維護單元測試的代碼。
- 還有就是編寫單元測試大概會花費日常開發的 10% 以上的時間,而項目時間總是比較緊,無法留出充裕的測試時間。
針對第一個問題,可用一個實驗回答。讓兩個在技術和經驗上近似的團隊,分別負責兩個規模近似的項目,其中一個進行單元測試,另一個不進行單元測試的時間差。
下表是兩個團隊的進度和輸出度量:
|
階段
|
不進行單元測試的團隊
|
進行單元測試的團隊
|
|---|---|---|
| 實施 | 7天 | 14天 |
| 集成 | 7天 | 2天 |
| 測試和修復缺陷 | 測試:3天 修復:3天 測試:3天 修復:2天 測試:1天 總計:12天 |
測試:3天 修復:1天 測試:1天 修復:1天 測試:1天 總計:7天 |
| 整體交付時間 | 26天 | 23天 |
| 客戶發現的缺陷數 | 71 | 11 |
針對第二個問題,可引用一份研究報告,20世紀70和90年代進行的研究表明:通常,20%的代碼包含了90%的缺陷。如果能找到這20%的代碼,那么就能大大提升測試效率。
但困難的就是如何找到包含最多問題的代碼。其實任何團隊都能告訴你哪個組件問題最多,那你就可以從這個組件開始測試。
針對第三個問題,目前的辦法是多寫多測,讓單元測試成為開發的一部分,不要苛求測試覆蓋率,先就測試影響業務流程的核心代碼。
二、測試替身
測試替身的作用是隔離被測代碼,加速執行測試,使執行變得確定,模擬特殊情況,暴露隱藏信息。
其中隔離被測代碼,使測試有針對性和容易理解,而利用測試替身實現的隔離,還有個副作用,那就是測試替身的速度要比本尊快很多。
測試替身的類型:
- Stub(測試樁):一個對象的所有方法只有一行,且各自返回一個適當的默認值。使用場景:只關心協作對象輸送的響應。
- Fake(偽造對象):可以返回硬編碼值,而每個測試可能需要有差異地實例化來返回不同值,模擬不同場景。使用場景:所依賴的服務或組件無法供測試使用,打樁產生了難以維護的糟糕代碼。
- Spy(測試間諜):用於記錄過去發生的情況,這樣測試在事后就知道所發生的一切。使用場景:將其作為參數傳遞被測函數中。
- Mock(模擬對象):是特殊的Spy,在一個特定情景下可配置行為的對象。使用場景:關心某些交互,即兩個對象之間的方法調用。
三、設計指南
- 避免測試中包含邏輯,不應該有switch、if-else等判斷語句,for、while等循環語句。以免測試難以閱讀和理解,難以復現,難以命名。
- 只測試一個關注點,一個工作單元只有一個最終結果,例如一個返回值、系統狀態的一個改變或對第三方對象的一個調用。
- 專注檢查行為而非實現,避免過度指定,只需檢查最終行為的正確性即可,既不要使用多個模擬對象,也不要對一個被測對象的純內部狀態進行斷言。
- 避免復雜的私有方法,不要直接測試 private 方法。
- 避免在構造函數中包含需要測試的代碼邏輯。
- 避免單例,單例模式會妨礙創建不同的變體。
- 使用 new 時要當心,實例化的對象,應該僅限於不會替換為測試替身的對象。
四、測試壞味道
1)可讀性
程序員用測試的方式來表達和驗證代碼的假設和預期行為。
閱讀測試代碼之后,就該理解代碼應當做什么。程序員運行那些測試時,就該了解代碼實際上在做什么。
- 問題:基本斷言缺乏意義,因為斷言的基本原理和意圖隱藏在看上去無意義的單詞和數字背后,造成難以理解。
- 改進:去掉魔法數字,改用斷言方法,使用編程語言內置的 API 語法。
- 問題:過度斷言很脆弱,並且掩蓋了整體廣度和深度之下的意圖。
- 改進:識別無關細節並移除。
- 問題:人格分裂是指一個測試檢查了多件事。
- 改進:去掉重復,將粗粒度的場景分離。
- 問題:過分保護是指在測試開頭增加守衛語句和空值檢查保護自己。
- 改進:去除冗余斷言,檢查要使真正的斷言通過所需的中間條件。
2)可維護性
代碼從不慢慢退化,而是直接奔潰。
測試也是如此,同樣脆弱,程序員編寫自動化的單元測試來盡可能地管理這種脆弱性。
大家都知道維護噩夢是什么,你絕對不希望你的測試代碼淪落其中。
- 問題:重復最明顯的是某一個數字或字符串在代碼中反復出現。
- 改進:將可變數據提煉到局部變量中。
- 問題:殘缺的文件路徑會使代碼無法轉移,只能在某個人的計算機中。
- 改進:避免絕對路徑,選擇相對路徑,用流來替換文件。
- 問題:像素完美出現的場景包括期望和實際產生的圖像完美匹配。
- 改進:用適當的抽象層次來表達測試,將背后的細節隱藏到自定義斷言中,進行模糊匹配。
3)可信度
軟件開發其實就是在修改、演進和維護代碼,如果不能信任測試,那么在即使看似最無辜的改動之后,仍然不能確信代碼是否能夠工作。
接下來會圍繞測試不可靠的問題來檢閱測試壞味道。
- 問題:永不失敗的測試不具有價值,給你虛假的安全感。
- 改進:養成運行測試的習慣,例如臨時修改被測代碼來故意觸發一次失敗。
- 問題:輕率承諾是指測試實際上沒有測試任何東西,或名不符實。
- 改進:確保斷言了一些事情,確切找出要檢查的行為,也更容易命名。
- 問題:降低期望就是降低了確定性與精確性的標准。
- 改進:提高門檻,使測試的期望更具體。
- 問題:有條件的測試是在一個測試方法內隱藏了秘密條件,使測試邏輯名不符實。
- 改進:確保測試在每個條件分支時都有機會失敗。
