17. 提供一個隨機值生成器
當邊界值都覆蓋了, 另一個能進一步改善測試覆蓋率的簡單方法就是生成隨機參數, 這樣每次執行測試都會有不同的輸入.
想要做到這點, 需要提供一個用來生成基本類型 (如: 浮點數, 整型, 字符串, 日期等) 隨機值的工具類. 生成器應該覆蓋各種類型的所有取值范圍.
如果測試時間比較短, 可以考慮再裹上一層循環, 覆蓋盡可能多的輸入組合. 下面的例子是驗證兩次轉換 little endian 和 big endian 字節序后是否返回原值. 由于測試過程很快, 可以讓它跑上個一百萬次.
void testByteSwapper()
{
for (int i = 0; i < 1000000; i++) {
double v0 = Random.getDouble();
double v1 = ByteSwapper.swap(v0);
double v2 = ByteSwapper.swap(v1);
assertEquals(v0, v2);
}
}
18. 每個特性只測一次
在測試模式下, 有時會情不自禁的濫用斷言. 這種做法會導致維護更困難, 需要極力避免. 僅對測試方法名指示的特性進行明確測試.
因為對于一般性代碼而言, 保證測試代碼盡可能少是一個重要目標.
19. 使用顯式斷言
應該總是優先使用 assertEquals(a, b) 而不是 assertTrue(a == b), 因為前者會給出為何導致測試失敗的更有意義的信息. 在事先不確定輸入值的情況下, 這條規則尤為重要, 比如之前使用隨機參數值組合的例子.
20. 提供反向測試
反向測試是指刻意編寫問題代碼, 來驗證魯棒性和能否正確的處理錯誤.
假設如下方法的參數如果傳進去的是負數, 會立馬拋出異常:
void setLength(double length) throws IllegalArgumentExcepti
可以用下面的方法來測試這個特例是否被正確處理:
try {
set Length(-1.0);
fail(); // If we get here, something went wrong
}
catch (IllegalArgumentException exception) {
// If we get here, all is fine
}
21. 代碼設計時謹記測試
編寫和維護單元測試的代價是很高的, 減少代碼中的公有接口和循環復雜度是降低成本, 使高覆蓋率測試代碼更易于編寫和維護的有效方法.
一些建議:
使類成員常量化, 在構造函數中進行初始化. 減少 setter 方法的數量.
限制過度使用繼承和公有虛函數.
通過使用友元類 (C++) 或包作用域 (Java) 來減少公有接口.
避免不必要的邏輯分支.
在邏輯分支中編寫盡可能少的代碼.
在公有和私有接口中盡量多用異常和斷言驗證參數參數的有效性.
限制使用快捷函數. 對于黑箱而言, 所有方法都必須一視同仁的進行測試. 考慮以下簡短的例子:
public void scale(double x0, double y0, double scaleFactor)
{
// scaling logic
}
public void scale(double x0, double y0)
{
scale(x0, y0, 1.0);
}
刪除后者可以簡化測試, 但用戶代碼的工作量也將略微增加.
22. 不要訪問預定的外部資源
單元測試代碼不應該假定外部的執行環境, 以便在任何時候/任何地方都能執行. 為了向測試提供必需的資源, 這些資源應該由測試本身提供.
比如一個解析某類型文件的類, 可以把文件內容嵌入到測試代碼里, 在測試的時候寫入到臨時文件, 測試結束再刪除, 而不是從預定的地址直接讀取.
23. 權衡測試成本
不寫單元測試的代價很高, 但是寫單元測試的代價同樣很高. 要在這兩者之間做適當的權衡, 如果用執行覆蓋率來衡量, 業界標準通常在 80% 左右.
很典型的, 讀寫外部資源的錯誤處理和異常處理就很難達到百分百的執行覆蓋率. 模擬數據庫在事務處理到一半時發生故障并不是辦不到, 但相對于進行大范圍的代碼審查, 代價可能太大了.
24. 合理安排測試優先次序
單元測試是典型的自底向上過程, 如果沒有足夠的資源測試一個系統的所有模塊, 就應該先把重點放在較底層的模塊.
25. 為測試失敗做好準備
考慮下面的這個例子:
Handle handle = manager.getHandle();
assertNotNull(handle);
String handleName = handle.getName();
assertEquals(handleName, "handle-01");
如果第一個斷言失敗, 緊接其后的語句會導致代碼崩潰, 剩下的測試都將不被執行. 任何時候都要為測試失敗做好準備, 避免單個失敗的測試項中斷整個測試套件的執行. 上面的例子可以重寫成:
Handle handle = manager.getHandle();
assertNotNull(handle);
if (handle == null) return;
String handleName = handle.getName();
assertEquals(handleName, "handle-01");
26. 寫測試用例重現 bug
每上報一個 bug, 都要寫一個測試用例來重現這個 bug (即無法通過測試), 并用它作為成功修正代碼的標準.
27. 了解局限
單元測試永遠無法證明代碼的正確性
一個跑失敗的測試可能表明代碼有錯誤, 但一個跑成功的測試什么也證明不了.
單元測試最有效的應用場合是驗證和, 以及 回歸測試: 當新功能增加和代碼進行重構的同時,會不會影響到舊功能的正確性.
參考資料
[1] 維基百科關于單元測試的定義: Unit Testing
[2] 白盒和黑盒測試的簡短描述: What is black box/white box testing?
[3] 我們最常用的 C++ 單元測試框架: CxxTest
[4] 我們最常用的 Java 單元測試框架: TestNG
原文轉自:http://www.wangyuxiong.com/archives/51625