單體測試指南
理想情況下在每次代碼簽入之前都要執行下測試套件。測試快就可以降低開發周轉時間。
2、單體測試必須完全自動化,不需要交互
測試套件通常經常執行,必須完全自動化才有用。如果結果需要人工檢查,該測試就不是真正的單體測試。
3、單體測試應便于運行
開發環境應該配置成通過一個單獨的命令或者一個按鈕點擊就可以運行單獨的測試和測試套件。
4、評估測試
對測試的運行進行覆蓋分析從而得到執行覆蓋并調查代碼的哪些部分執行,哪些部分沒有執行。
5、立刻修復失敗測試
每個開發者都應該確保在簽入后新的測試以及所有既有的測試都可以成功運行。
如果在日常測試執行中有一個測試失敗了,整個團隊必須停下手頭的工作來保證問題得到解決。
6、保持測試在單體層
單體測試和類的測試相關。每個普通類都要有一個測試類,類的行為應該在隔離的條件下測試。應該避免使用單體測試框架測試整個工作流程,因為這樣的測試會很慢并很難維護。會有需要流程測試的地方,但不應該作為單體測試的一部分,它必須獨立地設置和執行。
7、從簡單開始
一個簡單的測試要比根本沒有測試好。一個簡單的測試類可以建立起目標類的測試框架并可以驗證編譯環境、單體測試環境、執行環境和覆蓋分析工具是否具備以及是否正確,從而可以證明目標類是否是程序集的一部分以及是否可以訪問。
單體測試的入門程序可能像這樣:
void testDefaultConstruction() { Foo foo = new Foo(); assertNotNull(foo); } |
8、保存測試相互獨立
為了確保測試健壯并簡化維護,測試不能依賴其它測試以及測試執行的先后順序。
9、測試類和被測試類盡量近
如果被測試類是Foo,那么測試類就應該命名為FooTest(而不是TestFoo)并同Foo放在同一個包里面。將測試類放在單獨的目錄下會使其難于訪問和維護。
確保編譯環境的配置可以使得測試類不會進入生產庫或執行文件中。
10、合理命名測試
確保每個測試方法測試一個明顯的類功能并據此命名測試方法。典型的命名規則是test[what],比如testSaveAs(), testAddListener(), testDeleteProperty()等。
11、測試公開API
單體測試被定義為通過公開API測試類。有些測試工具可以實現類的私有方法的測試,但由于這會使得測試太過繁瑣并更難于維護因此需要避免。如果有一些類 的私有方法需要顯示地進行測試,考慮將其重構成工具類的公開方法。要這樣做就應該要改進總體設計,而不是僅僅為了幫助測試。
12、以黑盒考慮
作為第三方的類使用者來測試該類是否滿足需求。嘗試著讓它失效。
畢竟,開發者寫測試的同時也寫了被測試類,需要特別注意測試復雜邏輯。
14、微不足道的類也要測試
有人會推薦測試所有主要的情況,而可以忽略諸如簡單的類似setter和getter等微不足道方法。 然而,應該測試微不足道方法情況有幾個原因:
● 很難定義微不足道。對不同的人可能有不同的含義。
● 從黑盒測試的角度看無法知道代碼的哪部分是微不足道的。
● 由于拷貝-粘貼操作,微不足道的代碼也可能包含錯誤。
private double weight_; private double x_, y_; public void setWeight(int weight) { weight = weight_; // 錯誤 } public double getX() { return x_; } public double getY() { return x_; // 錯誤 } |
建議就是測試所有代碼,畢竟微不足道的代碼很容易測試。
15、首先關注執行覆蓋率
執行覆蓋不同于實際代碼覆蓋。一個測試的最初目標應該保證高的執行覆蓋。這可以確保代碼在某些參數下真正執行。有了這個,就可以去改善測試覆蓋了。注意實際代碼覆蓋很難確定(通常都很接近0%)。
考慮下面這個公開方法:
void setLength(double length); |
通過調用setLength(1.0) 你就可能得到100% 的執行覆蓋率。為了達到真正100%的實際測試覆蓋,必須使用所有可能的double值來調用該方法以確認它們的正確行為。這顯然是不可能的。
16、覆蓋邊界情況
確保覆蓋參數邊界的情況。對于數,測試負數、0、正數、最小、最大、NaN、無窮等情況。對于字符串,考慮空字符串、單個字母的字符串、非 ASCII的字符串、多字節的字符串等情況。對于集合,測試空集合、單個元素集合、第一個、最后一個等。對于日期,考慮1月1日、2月29日、12月31 日等。被測類會提示各個具體情況下的邊界情況。由于這些都可能是錯誤的根源,因此要盡可能多地測試這些情況。
17、提供隨機數生成器
在覆蓋了邊界情況后,進一步提高覆蓋率的一個簡單方法就是產生隨機數以使得每次測試都可以使用不同的輸入執行。
為了實現這個目標,可以提供一個生成double、integer、 string和dates等類型隨機數的實用類。生成器必須可以從各個類型的全范圍內生成值。
如果測試很快,可以考慮在一個循環內運行盡可能多的組合。下面的例子就是驗證通過一次大端和一次小端轉化是否可以得到原值。由于測試很快,每次可以根據不同的值執行100萬次。
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); } } |
在測試模式下,很容易去嘗試對每個測試中的每件事都斷言。這必須避免因為它使得維護更難。僅測試該測試方法的名字表明的功能。
對于一般代碼應該將測試代碼盡可能少作為一個目標。
19、使用顯式斷言
在assertEquals(a, b)和assertTrue(a == b) (之類的)之間優先選擇前者,因為它可以在測試失敗的時候給出關于失敗原因的更多有用的信息。 這對于上述提到的隨機參數組合時輸入值無法預先知道的情況尤為重要。
20、提供負面測試
負面測試是通過故意用錯代碼以驗證穩健性和適當的錯誤處理。
考慮下面這個如果傳入負數就會拋出異常的方法:
void setLength(double length) throws IllegalArgumentException; |
可以這樣來測試該特殊情況的正確行為:
try { setLength(-1.0); fail(); // If we get here, something went wrong } catch (IllegalArgumentException exception) { // If we get here, all is fine } |
21、設計代碼是考慮測試
編寫和維護單體測試的代價很高,使公開API最小化并降低代碼的循環復雜度是降低成本并使得高覆蓋率的測試代碼更快編寫和更易于維護的方式。
一些建議:
● 通過構造時候確定狀態來使得成員類不可變。這會減少對setter方法的需求。
● 限制過渡使用繼承和虛的公開方法。
● 減少利用了友元類(C++)或包范圍(Java)的公開API。
● 避免不必要的分支。
● 在分支內的代碼盡可能少。
● 盡可能使用異常和斷言來驗證分別在公開和私有API中的參數。
● 限制使用幫助方法。從一個黑盒測試的角度每個方法都必須一樣地測試。考慮如下這個小例子:
public void scale(double x0,double y0,double scaleFactor) { // scaling logic } public void scale(double x0, double y0) scale(x0, y0, 1.0); } |
省去后面的就可以簡化測試,但代價是客戶端代碼會有額外的工作。
編寫測試文件的時候應該不能利用將要執行環境的上下文信息以使得他們可以在任何時間任何地方運行。為了給測試提供所需的資源,這些資源應該通過測試自身提供。
現在考慮解析某種類型文件的類的情況。應該將文件的內容放在測試內,在測試開始前將其寫入一個臨時文件并在測試完成后刪除掉文件,而不是從預定義的路徑選擇一個示例文件。
23、了解測試成本
不寫單體測試代價很高,寫單體測試代價也很高。這是兩者之間的平衡,從執行覆蓋率考慮一般的行業標準是大約是80%。
通常比較困難達到完全覆蓋的區域是處理外部資源的錯誤或異常。在一個交易中模擬數據庫崩潰是完全有可能的,但較之作為替代方法的深度代碼檢查通常代價過高。
24、測試優先級排序
單體測試通常是一個至底而上的流程,如果沒有測試系統所有部分所需的足夠資源就應該優先考慮最底層。
25、考慮到測試代碼失敗
考慮下這個簡單的實例:
Handle handle = manager.getHandle(); String handleName = handle.getName(); |
如果第一個斷言是假的,接下來的語句就會崩潰,余下的測試也就不會被執行了。所以應該考慮單個測試代碼失效不會使得這個測試套件無法執行。通常可以改寫成這樣:
Handle handle = manager.getHandle(); String handleName = handle.getName(); |
26、編寫測試來重現缺陷
如果報告了一個缺陷,就應該寫一個測試來重現該缺陷(比如一個失敗的測試)并使用該測試作為是否成功修復代碼的標準。
27、認識局限性
單體測試永遠不能證明代碼的正確性!
一個失敗的測試意味著代碼含有錯誤,而一個成功的測試不能證明任何東西。
單體測試的最普遍的應用就是驗證和記錄底層的需求以及回歸測試:驗證代碼在變遷和重構的過程中一直保持穩定。
因此,單體測試永遠無法替代前期設計和健全的開發流程。單體測試可以作為既有開發方法的一個寶貴補充。
后記:
對于第9點”測試類和被測試類盡量近”和第14點”微不足道的類也要測試”,我不是很贊同,實踐的時候也沒有完全遵守。但為了忠實于原文沒有做 擅自修改。大家在學習和實踐的時候可能也會接觸一些相悖的觀點和理論,不要擔心,選擇最適合你嘗試下就知道了。同時,歡迎說出你的故事!
posted on 2012-07-26 09:46 順其自然EVO 閱讀(647) 評論(0) 編輯 收藏 所屬分類: 測試學習專欄