1. 保持單元測(cè)試小巧, 快速
理論上, 任何代碼提交前都應(yīng)該完整跑一遍所有測(cè)試套件. 保持測(cè)試代碼執(zhí)行快能夠縮短迭代開發(fā)周期.
2. 單元測(cè)試應(yīng)該是全自動(dòng)/非交互式的
測(cè)試套件通常是定期執(zhí)行的, 執(zhí)行過程必須完全自動(dòng)化才有意義. 輸出結(jié)果需要人工檢查的測(cè)試不是一個(gè)好的單元測(cè)試.
3. 讓單元測(cè)試很容易跑起來
對(duì)開發(fā)環(huán)境進(jìn)行配置, 好是敲一條命令或是點(diǎn)擊一個(gè)按鈕就能把單個(gè)測(cè)試用例或測(cè)試套件跑起來.
4. 對(duì)測(cè)試進(jìn)行評(píng)估
對(duì)執(zhí)行的測(cè)試進(jìn)行覆蓋率分析, 得到精確的代碼執(zhí)行覆蓋率, 并調(diào)查哪些代碼未被執(zhí)行.
5. 立即修正失敗的測(cè)試
每個(gè)開發(fā)人員在提交前都應(yīng)該保證新的測(cè)試用例執(zhí)行成功, 當(dāng)有代碼提交時(shí), 現(xiàn)有測(cè)試用例也都能跑通.
如果一個(gè)定期執(zhí)行的測(cè)試用例執(zhí)行失敗, 整個(gè)團(tuán)隊(duì)?wèi)?yīng)該放下手上的工作先解決這個(gè)問題.
6. 把測(cè)試維持在單元級(jí)別
單元測(cè)試即類 (Class) 的測(cè)試. 一個(gè) “測(cè)試類” 應(yīng)該只對(duì)應(yīng)于一個(gè) “被測(cè)類”, 并且 “被測(cè)類” 的行為應(yīng)該被隔離測(cè)試. 必須謹(jǐn)慎避免使用單元測(cè)試框架來測(cè)試整個(gè)程序的工作流, 這樣的測(cè)試即低效又難維護(hù). 工作流測(cè)試 (譯注: 指跨模塊/類的數(shù)據(jù)流測(cè)試) 有它自己的地盤, 但它絕不是單元測(cè)試, 必須單獨(dú)建立和執(zhí)行.
7. 由簡(jiǎn)入繁
再簡(jiǎn)單的測(cè)試也遠(yuǎn)遠(yuǎn)勝過完全沒有測(cè)試. 一個(gè)簡(jiǎn)單的 “測(cè)試類” 會(huì)促使建立 “被測(cè)類” 基本的測(cè)試骨架, 可以對(duì)構(gòu)建環(huán)境, 單元測(cè)試環(huán)境, 執(zhí)行環(huán)境以及覆蓋率分析工具等有效性進(jìn)行檢查, 同時(shí)也可以證明 “被測(cè)類” 能夠被整合和調(diào)用.
下面便是單元測(cè)試版的 Hello, world! :
void testDefaultConstruction()
{
Foo foo = new Foo();
assertNotNull(foo);
}
8. 保持測(cè)試的獨(dú)立性
為了保證測(cè)試穩(wěn)定可靠且便于維護(hù), 測(cè)試用例之間決不能有相互依賴, 也不能依賴執(zhí)行的先后次序.
9. Keep tests close to the class being tested
[譯注: 有意翻譯該規(guī)則, 個(gè)人認(rèn)為本條規(guī)則值得商榷, 大部分 C++, Objective-C和 Python 庫均把測(cè)試代碼從功能代碼目錄中獨(dú)立出來, 通常是創(chuàng)建一個(gè)和 src 目錄同級(jí)的 tests 目錄, 被測(cè)模塊/類名之前也常常 不加 Test 前綴. 這么做保證功能代碼和測(cè)試代碼隔離, 目錄結(jié)構(gòu)清晰, 并且發(fā)布源碼的時(shí)候更容易排除測(cè)試用例.]
If the class to test is Foo the test class should be called FooTest (not TestFoo) and kept in the same package (directory) as Foo. Keeping test classes in separate directory trees makes them harder to access and maintain.
Make sure the build environment is configured so that the test classes doesn't make its way into production libraries or executables.
10. 合理的命名測(cè)試用例
確保每個(gè)方法只測(cè)試 “被測(cè)類” 的一個(gè)明確特性, 并相應(yīng)的命名測(cè)試方法. 典型的命名俗定是test[what], 比如 testSaveAs(), testAddListener(), testDeleteProperty() 等.
11. 只測(cè)公有接口
單元測(cè)試可以被定義為 通過類的公有 API 對(duì)類進(jìn)行測(cè)試. 一些測(cè)試工具允許測(cè)試一個(gè)類的私有成員, 但這種做法應(yīng)該避免, 它讓測(cè)試變得繁瑣而且更難維護(hù). 如果有私有成員確實(shí)需要進(jìn)行直接測(cè)試, 可以考慮把它重構(gòu)到工具類的公有方法中. 但要注意這么做是為了改善設(shè)計(jì), 而不是幫助測(cè)試.
12. 看成是黑盒
站在第三方使用者的角度, 測(cè)試一個(gè)類是否滿足規(guī)定的需求. 并設(shè)法讓它出問題.
13. 看成是白盒
畢被測(cè)試類是程序員自寫自測(cè)的, 應(yīng)該在復(fù)雜的邏輯部分多花些精力測(cè)試.
14. 芝麻函數(shù)也要測(cè)試
通常建議所有重要的函數(shù)都應(yīng)該被測(cè)試到, 一些芝麻方法比如簡(jiǎn)單的 setter 和 getter 都可以忽略. 但是仍然有充分的理由支持測(cè)試芝麻函數(shù):
芝麻 很難定義. 對(duì)于不同的人有不同的理解.
從黑盒測(cè)試的觀點(diǎn)看, 是無法知道哪些代碼是芝麻級(jí)別的.
即便是再芝麻的函數(shù), 也可能包含錯(cuò)誤, 通常是 “復(fù)制粘貼” 代碼的后果:
private double weight_;
private double x_, y_;
public void setWeight(int weight)
{
weight = weight_; // error
}
public double getX()
{
return x_;
}
public double getY()
{
return x_; // error
}
因此建議測(cè)試所有方法. 畢芝麻用例也容易測(cè)試.
15. 先關(guān)注執(zhí)行覆蓋率
區(qū)別對(duì)待 執(zhí)行覆蓋率 和 實(shí)際測(cè)試覆蓋率. 測(cè)試的初目標(biāo)應(yīng)該是確保較高的執(zhí)行覆蓋率. 這樣能保證代碼在 少量 參數(shù)值輸入時(shí)能執(zhí)行成功. 一旦執(zhí)行覆蓋率就緒, 就應(yīng)該開始改進(jìn)測(cè)試覆蓋率了. 注意, 實(shí)際的測(cè)試覆蓋率很難衡量 (而且往往趨近于 0%).
思考以下公有方法:
void setLength(double length);
調(diào)用 setLength(1.0)你可能會(huì)得到 100% 的執(zhí)行覆蓋率. 但要達(dá)到 100% 的實(shí)際測(cè)試覆蓋率, 有多少個(gè) double 浮點(diǎn)數(shù)這個(gè)方法就必須被調(diào)用多少次, 并且要一一驗(yàn)證行為的正確性. 這無疑是不可能的任務(wù).
16. 覆蓋邊界值
確保參數(shù)邊界值均被覆蓋. 對(duì)于數(shù)字, 測(cè)試負(fù)數(shù), 0, 正數(shù), 小值, 大值, NaN (非數(shù)字), 無窮大等. 對(duì)于字符串, 測(cè)試空字符串, 單字符, 非 ASCII 字符串, 多字節(jié)字符串等. 對(duì)于集合類型, 測(cè)試空, 1, 個(gè), 后一個(gè)等. 對(duì)于日期, 測(cè)試 1月1號(hào), 2月29號(hào), 12月31號(hào)等. 被測(cè)試的類本身也會(huì)暗示一些特定情況下的邊界值. 要點(diǎn)是盡可能徹底的測(cè)試這些邊界值, 因?yàn)樗鼈兌际侵饕?“疑犯”.
17. 提供一個(gè)隨機(jī)值生成器
當(dāng)邊界值都覆蓋了, 另一個(gè)能進(jìn)一步改善測(cè)試覆蓋率的簡(jiǎn)單方法就是生成隨機(jī)參數(shù), 這樣每次執(zhí)行測(cè)試都會(huì)有不同的輸入.
想要做到這點(diǎn), 需要提供一個(gè)用來生成基本類型 (如: 浮點(diǎn)數(shù), 整型, 字符串, 日期等) 隨機(jī)值的工具類. 生成器應(yīng)該覆蓋各種類型的所有取值范圍.
如果測(cè)試時(shí)間比較短, 可以考慮再裹上一層循環(huán), 覆蓋盡可能多的輸入組合. 下面的例子是驗(yàn)證兩次轉(zhuǎn)換 little endian 和 big endian 字節(jié)序后是否返回原值. 由于測(cè)試過程很快, 可以讓它跑上個(gè)一百萬次.
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. 每個(gè)特性只測(cè)一次
在測(cè)試模式下, 有時(shí)會(huì)情不自禁的濫用斷言. 這種做法會(huì)導(dǎo)致維護(hù)更困難, 需要極力避免. 僅對(duì)測(cè)試方法名指示的特性進(jìn)行明確測(cè)試.
因?yàn)閷?duì)于一般性代碼而言, 保證測(cè)試代碼盡可能少是一個(gè)重要目標(biāo).
19. 使用顯式斷言
應(yīng)該總是優(yōu)先使用 assertEquals(a, b) 而不是 assertTrue(a == b), 因?yàn)榍罢邥?huì)給出更有意義的測(cè)試失敗信息. 在事先不確定輸入值的情況下, 這條規(guī)則尤為重要, 比如之前使用隨機(jī)參數(shù)值組合的例子.
20. 提供反向測(cè)試
反向測(cè)試是指刻意編寫問題代碼, 來驗(yàn)證魯棒性和能否正確的處理錯(cuò)誤.
假設(shè)如下方法的參數(shù)如果傳進(jìn)去的是負(fù)數(shù), 會(huì)立馬拋出異常:
void setLength(double length) throws IllegalArgumentException
可以用下面的方法來測(cè)試這個(gè)特例是否被正確處理:
try {
setLength(-1.0);
fail(); // If we get here, something went wrong
}
catch (IllegalArgumentException exception) {
// If we get here, all is fine
}
21. 代碼設(shè)計(jì)時(shí)謹(jǐn)記測(cè)試
編寫和維護(hù)單元測(cè)試的代價(jià)是很高的, 減少代碼中的公有接口和循環(huán)復(fù)雜度是降低成本, 使高覆蓋率測(cè)試代碼更易于編寫和維護(hù)的有效方法.
一些建議:
使類成員常量化, 在構(gòu)造函數(shù)中進(jìn)行初始化. 減少 setter 方法的數(shù)量.
限制過度使用繼承和公有虛函數(shù).
通過使用友元類 (C++) 或包作用域 (Java) 來減少公有接口.
避免不必要的邏輯分支.
在邏輯分支中編寫盡可能少的代碼.
在公有和私有接口中盡量多用異常和斷言驗(yàn)證參數(shù)參數(shù)的有效性.
限制使用快捷函數(shù). 對(duì)于黑箱而言, 所有方法都必須一視同仁的進(jìn)行測(cè)試. 考慮以下簡(jiǎn)短的例子:
public void scale(double x0, double y0, double scaleFactor)
{
// scaling logic
}
public void scale(double x0, double y0)
{
scale(x0, y0, 1.0);
}
刪除后者可以簡(jiǎn)化測(cè)試, 但用戶代碼的工作量也將略微增加.
22. 不要訪問預(yù)設(shè)的外部資源
單元測(cè)試代碼不應(yīng)該假定外部的執(zhí)行環(huán)境, 以便在任何時(shí)候/任何地方都能執(zhí)行. 為了向測(cè)試提供必需的資源, 這些資源應(yīng)該由測(cè)試本身提供.
比如一個(gè)解析某類型文件的類, 可以把文件內(nèi)容嵌入到測(cè)試代碼里, 在測(cè)試的時(shí)候?qū)懭氲脚R時(shí)文件, 測(cè)試結(jié)束再刪除, 而不是從預(yù)定的地址直接讀取.
23. 權(quán)衡測(cè)試成本
不寫單元測(cè)試的代價(jià)很高, 但是寫單元測(cè)試的代價(jià)同樣很高. 要在這兩者之間做適當(dāng)?shù)臋?quán)衡, 如果用執(zhí)行覆蓋率來衡量, 業(yè)界標(biāo)準(zhǔn)通常在 80% 左右.
很典型的, 讀寫外部資源的錯(cuò)誤處理和異常處理就很難達(dá)到百分百的執(zhí)行覆蓋率. 模擬數(shù)據(jù)庫在事務(wù)處理到一半時(shí)發(fā)生故障并不是辦不到, 但相對(duì)于進(jìn)行大范圍的代碼審查, 代價(jià)可能太大了.
24. 安排測(cè)試優(yōu)先次序
單元測(cè)試是典型的自底向上過程, 如果沒有足夠的資源測(cè)試一個(gè)系統(tǒng)的所有模塊, 就應(yīng)該先把重點(diǎn)放在較底層的模塊.
25. 測(cè)試代碼要考慮錯(cuò)誤處理
考慮下面的這個(gè)例子:
Handle handle = manager.getHandle();
assertNotNull(handle);
String handleName = handle.getName();
assertEquals(handleName, "handle-01");
如果個(gè)斷言失敗, 后續(xù)語句會(huì)導(dǎo)致代碼崩潰, 剩下的測(cè)試都無法執(zhí)行. 任何時(shí)候都要為測(cè)試失敗做好準(zhǔn)備, 避免單個(gè)失敗的測(cè)試項(xiàng)中斷整個(gè)測(cè)試套件的執(zhí)行. 上面的例子可以重寫成:
Handle handle = manager.getHandle();
assertNotNull(handle);
if (handle == null) return;
String handleName = handle.getName();
assertEquals(handleName, "handle-01");
26. 寫測(cè)試用例重現(xiàn) bug
每上報(bào)一個(gè) bug, 都要寫一個(gè)測(cè)試用例來重現(xiàn)這個(gè) bug (即無法通過測(cè)試), 并用它作為成功修正代碼的檢驗(yàn)標(biāo)準(zhǔn).
27. 了解局限
單元測(cè)試永遠(yuǎn)無法證明代碼的正確性!!
一個(gè)跑失敗的測(cè)試可能表明代碼有錯(cuò)誤, 但一個(gè)跑成功的測(cè)試什么也證明不了.
單元測(cè)試有效的使用場(chǎng)合是在一個(gè)較低的層級(jí)驗(yàn)證并文檔化需求, 以及 回歸測(cè)試: 開發(fā)或重構(gòu)代碼時(shí),不會(huì)破壞已有功能的正確性.
本站文章版權(quán)歸原作者及原出處所有 。內(nèi)容為作者個(gè)人觀點(diǎn), 并不代表本站贊同其觀點(diǎn)和對(duì)其真實(shí)性負(fù)責(zé),本站只提供參考并不構(gòu)成任何投資及應(yīng)用建議。本站是一個(gè)個(gè)人學(xué)習(xí)交流的平臺(tái),網(wǎng)站上部分文章為轉(zhuǎn)載,并不用于任何商業(yè)目的,我們已經(jīng)盡可能的對(duì)作者和來源進(jìn)行了通告,但是能力有限或疏忽,造成漏登,請(qǐng)及時(shí)聯(lián)系我們,我們將根據(jù)著作權(quán)人的要求,立即更正或者刪除有關(guān)內(nèi)容。本站擁有對(duì)此聲明的最終解釋權(quán)。