如何設計單元測試?
單元測試設計方法
單元測試用例,和普通測試用例的設計,沒有太多不同,常見的就是等價類劃分、邊界值分析等。而測試用例的設計其實也是開發者應該掌握的基本技能。
等價類劃分
把所有輸入劃分為若干分類,從每個分類中選取少數有代表性的數據做為測試用例。
例如,一個方法計算輸入參數的絕對值的倒數,如果是輸入是 0,則拋異常。那么對這個方法寫測試的話,就應該有三個等價類,輸入是負數、0 以及正數。所以我可以選取一個負數、一個正數以及 0 來設計三個測試用例。
再舉個例子,某個方法是根據醫生的認證狀態,發送不同的消息。那么等價類可能有三種,未認證、普通認證但無權威認證、普通認證且權威認證,某些情況下可能還會包括無普通認證但有威認證。
邊界值分析
邊界值是指劃分等價類后,在邊界附近的一些輸入數據,這些輸入往往是最容易出錯的。
例如,對于上面計算絕對值的倒數的例子,那么邊界值就包括 Integer.min、-1、0、1、Integer.max 等。再舉個例子,文本框輸入范圍為 1 - 255 個字符,那么等價類按輸入長度劃分有三類 0、1 到 255、大于 255,而邊界值則包括 0、1、2、254、255、256 等。
其他類似于空數組、數組的第一個和最后一個、報表的第一行和最后一行等等,也是屬于邊界值,需要特別關注。
其他方法
除了上面提到的幾種,測試設計方法還有幾種常用的:
場景法。場景法是根據模塊實際使用的場景,例如 API 的實際調用方法、系統的實際需求場景和處理邏輯創建的用例。這種方法比較直觀,并且用例貼近實際需求的,不可忽視。
錯誤推測。錯誤推測其實就是憑直覺,考慮最容易出錯的情況來設計用例。例如,我們直到新用戶、重復請求、并發、弱網、大數據量等情況都是非常容易出錯的,那么可以針對性的設計用例。錯誤推測需要測試設計者比較熟悉業務邏輯,并且經驗豐富。
其他還有因果圖、正交法等方法,這里就不說了。
覆蓋率
如果按照前面的用例設計方法,可能會設計出很多用例。我們不可能也沒有必要把每一個用例都寫成單元測試。
怎么確認用例是否足夠呢?一個很重要的參考指標就是代碼覆蓋率。
覆蓋率指標
常用的覆蓋率指標有四種:
語句覆蓋:每條語句至少執行一次。
分支覆蓋:每個分支至少有一次為真、一次為假。
條件覆蓋:每個分支的每個條件至少有一次為真、一次為假。
路徑覆蓋:對所有的分支、循環等可能的路徑,至少都要覆蓋一次。
我們以這個簡單的代碼為例,看看這四種覆蓋率到底是什么意思。
if (a && b) {
// X
}
// Y
if (c || d) {
// X
}
語句覆蓋。只需要一個測試用例,讓 a && b 和 c || d 都為真,系統會依次執行 X、Y、Z 三個的代碼段,就能做到語句覆蓋。
分支覆蓋。至少需要兩個測試用例,讓 a && b 和 c || d 都各為真假,例如用例1 a && b 為真和 c || d 為假,用例2 則反過來,既可讓兩個條件分支都各為真一次,為假一次。
條件覆蓋。至少需要四個測試用例,條件 a 和 b 的四種組合都要執行一次,條件 c 和 d 的四種組合也都要執行一次。
路徑覆蓋。至少需要八個測試用例,條件 a、b、c 和 d 的所有組合都要執行一次。
可以看到,要做到條件覆蓋甚至路徑覆蓋,會需要非常多的測試用例。一般情況,對于復雜的邏輯,單元測試做到分支覆蓋就不錯了,必要的話再做更多完全的覆蓋。
Jacoco 覆蓋
Jacoco 的覆蓋率略有不同,這里簡單說一下。
指令覆蓋(Instructions),覆蓋所有的 Java 代碼指令。
分支覆蓋(Branches),和上面的分支覆蓋基本是一樣的。
圈復雜度覆蓋(Cyclomatic Complexity),可以認為就是路徑覆蓋率。
語句覆蓋(Lines),和上面的語句覆蓋基本是一樣的。
方法覆蓋(Methods),覆蓋所有的方法。
類覆蓋(Classes),覆蓋所有的類。
怎么寫有效的單元測試?
到現在,相信大家對怎么寫單元測試應該有一定概念了。但是很多人也會有疑問:
單元測試耗費太多時間,會不會降低生產效率?
單元測試會不會很難維護?比如修改代碼時還總是需要修改單元測試。
關于第一個問題,相信大家應該都能理解,如果我們在開發時發現 BUG,那么解決它是很容易的;但是一旦到了集成、驗收甚至上線之后,那么要解決它就要花費比較大的代價了。業界很早就有共識,并且有不少數據可以證明,有效的單元測試雖然要花費更多編碼時間,但是可以很大的減少項目的集成、測試和維護成本。
注意上面提到很重要一點是,單元測試必須是有效的,如果我們發現單元測試很難維護,那往往是因為我們沒有寫出有效的單元測試。
不是所有的代碼都需要單元測試
寫單元測試我們也需要考慮投入產出比,例如下面這些情況,寫單元測試的投入產出比可能會較差。
短期的或者一次性的項目,例如 Demo、數據更新腳本。
業務簡單的,不含太多邏輯的模塊。例如獲取或者查找一個數據,或者沒有分支條件的業務邏輯等。
UI 層,相對而言比較難做單元測試,除非 UI 本身就有比較復雜的邏輯(其實某些 UI 框架也提供了單元測試工具)。
那么那些情況下要寫單元測試呢?簡單來說,就是兩類。
邏輯復雜、不容易理解、容易出錯的模塊。例如,計算閏年的方法、訂單下單等。
公共模塊或者核心的業務模塊。
即使對于需要寫單元測試的模塊,我們也應該關注最核心最重要的測試用例,而沒必要單純的追求覆蓋率,或者追求條件覆蓋甚至路徑覆蓋,一般做到分支覆蓋就可以了。另外一個有效的方法是,對于出現的每一個 BUG,添加一個單元測試。
單元測試應該是穩定的
這里穩定的第一個含義是,單元測試不應該經常需要修改。如果單元測試經常因為底層實現邏輯的變動而需要修改,那一定不是好的單元測試。也就是說,被測單元的接口應該是穩定的、設計良好的、易于擴展的。
穩定的第二個含義是,單元測試的結果應該是穩定的。如果在不同的環境、不同的情況運行單元測試,會返回不同的結果,那就不是好的單元測試。如果測試需要依賴特定的數據、文件等,那需要有前置的初始化腳本確保依賴的數據、文件在所有環境都存在并且是一致的。
單元測試應該是灰盒測試
單元測試應該覆蓋核心邏輯的各種分支、邊界及異常,但是避免涉及易變的實現邏輯。也就是說,我們不應該把單元測試當成完全的白盒測試,但也不是黑盒測試,而應該把它當成介于白盒和黑盒之間的灰盒測試。
被測代碼應該是抽象良好的
如果我們發現一段代碼很難編寫單元測試,常常是因為這段代碼沒有符合良好的抽象規范,比如沒有使用 DI、不符合單一職責原則、或者依賴了全局的公共變量和方法等等。我們可以考慮優化這段代碼,再來嘗試單元單元測試。
談談到底什么是抽象,以及軟件設計的抽象原則 介紹了軟件抽象的原則,這里就不再重復了。
編碼時就應該同時寫好單元測試
這樣我們才能在調試時就發揮單元測試的優勢,對代碼的任何修改都能得到即時反饋。如果是后面再補充單元測試,一方面對實現可能已經不太熟悉了,編寫測試的代價更大了;另一方面,單元測試能發揮的作用也變小了。不過即使這樣,對那些需要長遠維護的項目,編寫單元測試也還是很有用的。
單元測試的代碼質量也很重要
單元測試也是代碼,也是需要不斷維護的。所以我們不應該隨隨便便的去寫單元測試,而是要把他們也當成普通代碼一樣,要做到高質量、模塊化、可維護。
為什么要寫單元測試之終極原因
終極原因是,作為一名優秀的工程師,如果被 QA 和產品經理 Challenge 有 BUG,能忍嗎?而我們工程師當然要用工程師 Style 的測試方法,那就是自動化的單元測試了,不是嗎?
文章來源:網絡 版權歸原作者所有
上文內容不用于商業目的,如涉及知識產權問題,請權利人聯系小編,我們將立即處理