什么是UT?
UT(Unit Test)即單元測試
UT有什么價值?
大部分的開發都不喜歡寫UT,原因無非以下幾點:
- 產品經理天天催進度,哪有時間寫UT
- UT是測試自己的代碼,自測?那要QA何用?
- 自測能測出bug?都是基於自身思維,就像考試做完第一遍,第二遍檢查一樣,基本檢查不出什么東西
- UT維護成本太高,投入產出比太低
- 不會寫UT
總之有無數種理由不想寫UT,作為工作不到三年的菜鳥深有體會。之前在點評工作的時候,團隊的“UT”都集中於RPC的服務端。
為啥帶雙引號? 因為RPC的服務端沒有頁面可以功能測試,部署到測試環境測試太麻煩,只能寫UT了。在這個場景下我認為叫“驗證”更合適,驗證不等於測試。 驗證往往只寫主邏輯是否通過,且就一個Case,且沒有Assert,有的是System.out。
本人實習的時候做測試的,那時候知道一個測試模型。如下圖:
圖的意思就是越底層做的測試效果越好,越往上則越差。也就是說大部分公司現在做的功能測試其實是效果最差的一種測試方式。 另外,QA界有個現場:大家都知道功能測試沒技術含量,那如何使自己突出呢?答案就是:自動化測試。現實是沒幾個公司能做好自動化測試, 業界做的比較好的百度算一個。
那么為啥自動化測試這么難做的?在這個模型當中,越往上黑盒越大,自動化測試難度就越大。 這句話反過來就是越往下自動化測試就越好做?沒錯,UT其實是最容易實現且效果最好的自動化測試。 所以在很多公司出現一種現場:QA寫UT。
原因總結一下就兩點:開發不願意寫UT,QA想自動化測試解放自己。 以上的模型只是理論上說明UT具有巨大的價值,但是真的如此么?我只想說,只有真正嘗到UT的好處的甜頭才會意識到UT的價值。
Unit Test & Intergration Test
單元測試和集成測試的界線我相信大部分開發也是不清晰的。個人理解單元測試針對於一塊業務邏輯最小的單元,太抽象。物理上可以簡單理解為一個類的方法, 可以是public方法也可以是private方法。一個單元測試不應該包含外部依賴的邏輯,反之就是集成測試了。
問題的核心就在於此。
一個service的一個接口實現可能依賴很多第三方:
1.本地其它的service
2.dao調用
3.rpc調用
4.微服務調用。
如下圖:
也就是說你的單元測試,真正調用了外部依賴那就是集成測試。這其實很常見對不?我們先說這種情況下如何集成測試。
Local Integration Test
本地集成測試也就是說不依賴與其他進程。包括:service依賴其他本地service或者dao的情況。在講述如何集成測試之前,我們先理一下測試模型,測試主要包含三塊內容:1.數據准備 2.執行邏輯 3.輸出驗證。
第一步:數據准備
在本地集成測試里,數據來源基本上來自於dao,dao來自於sql。也就是在執行一個case之前,執行一些sql腳本,數據庫則使用h2這類memory database, 切記不要依賴公司測試環境的db。
下圖是使用spring-test框架的一個case,可以在case執行之前准備我們所需要的各種數據, 另外在執行完case之后,執行clean.sql腳本來清理臟數據。這里也說明一個case的執行環境是完全獨立的,case之間互不干擾,這很重要。
第二步:執行邏輯最簡單,就是調用一下我們測試的方法即可
第三步:驗證
集成測試一般是調用service,或者dao的接口驗證。
舉個例子:CRUD操作的集成測試
- 調用C接口
- 調用R接口,驗證C成功
- 調用U接口
- 調用R接口,驗證U成功
- 調用D接口
- 調用R接口,驗證D成功
Remote Integration Test
假設我們一個service實現依賴某個RPC Service
第一步:數據准備
跑到別人家的數據庫插幾條數據?或者跟PRC Service的Owner商量好,搭一個測試環境供我們測試?有些公司還真有專門的自動化測試環境,那么即使有測試環境,那如何實現各種case場景下,第三方Service很配合的返回數據給我們?想想都蛋疼。
第二步:執行方法
假設我們成功的解決了第一步中的問題,皆大歡喜。現在來看第二步,假設我們的service里面調用了另一個RPC Service創建了很多數據,跑了無數次case,結果....RPC Service對應的數據庫都是我們的臟數據,如何清理?而且他們敢隨便刪數據嗎?想想也蛋疼。
第三步:輸出驗證
假設我們又愉快的解決了第二步中的問題。現在來看第三步,假設我們的方法執行最終輸出是創建了一個訂單,訂單當然是調用訂單Service接口了,那么我們如何驗證訂單是否成功創建了呢?或許可以調用訂單Service查詢訂單的接口來驗證。很明顯大多數情況下並沒有這么完美。想想也蛋疼呀。
通過以上分析,Local Integration Test是可行的,Remote Integration Test基本不可行。
那么有沒有什么辦法解決呢?答案就是Mock
- 第一步:Mock RPC Service 想返回什么數據就返回什么數據
- 第二步:還是Mock接口,想調用幾次就調用幾次
- 第三步:這一步等到下面講完單元測試就明白了
Unit Test
上面我們談到Mock可以解決外部依賴的問題,現在有很多Mock的開源框架比如:mockito。那么問題來了,既然我們可以mock第三方遠程依賴,為何不mock dao、local service呢?沒錯外部依賴全部mock掉,就是單元測試了。因為我們只關心所測試的方法的業務邏輯,也就是真正高內聚的邏輯單元了。如下圖:
好處如下:
- 沒有什么數據是造不出來的,通通返回Mock的對象
- 代碼中的異常處理代碼,也可以通過mock接口,使之拋出異常
- 不產生任何臟數據
- 跑case更快了,因為不用啟動整個項目,相當於Main方法
有人會說,都mock了還測試個蛋蛋。
這就是對於單元測試的理解了,單元測試應該只針對於目標方法的業務邏輯測試,dao、其它service應該在它們自身的單元測試去測試。對於依賴的第三方,我們應該信任它們能正確的完成我們所預期的。這句話很難理解對不對?
舉幾個例子
例子一:方法的最后是執行dao的create操作,那么該如何驗證?
我們應該驗證的內容是:
- dao的create方法被調用了
- 調用次數是對的
- 調用參數也是對的
沒錯,只要這三個驗證通過,那么這個case執行就是通過的。因為我們相信dao的create操作能正確的完成我們所預期的,只要我們調用了正確的次數並且參數都是對的。
dao的執行的正確性保證是在該dao的單元測試做的。 在Remote Integration Test里面第三步驗證道理是一樣的,我們應該驗證RPC接口被調用了且次數和參數都是對的,那么我們的case就算通過了,至於,RPC服務端是否正確執行是它們的事情不是我們所關心的。 Mockito框架的verify接口就是做這件事情的。如果你理解了上述內容,那么你就開竅了,UT不在變得這么難寫。
什么時候用單元測試,什么時候用集成測試?
在本人的實踐中摸索發現,對於簡單的業務,比如crud型的瘦service,比較適合於集成測試。
以下情況適合於單元測試:
- Util類
- 含有遠程調用的方法
- 輸入少,業務邏輯復雜的方法
- 需要異常處理的方法
case細到什么程度為好?
這個問題也是比較經典的,一個方法要是所有的路徑都覆蓋到,那么要寫很多的case,說真的累死人。我的建議是兩個原則:1.核心邏輯,容易出錯的邏輯一定要覆蓋到 2. 根據自己的時間。 沒必要寫的非常多,畢竟case維護成本很高,業務邏輯一改,case得跟着改。
總結
本人目前在從事於開源項目(Apollo(配置中心) )研發,開源項目對代碼質量要求相對來說高一些,UT當然是很重要的一環。剛開始也不會寫UT,當然態度上也不重視UT。
老大的代碼UT覆蓋率很高,抱着對開源負責的態度慢慢接受學習UT,到后來嘗了幾次甜頭后,發現UT真的很實用,價值也很高,但是很遺憾UT被大部分開發所忽略。當然本人對UT的理解、實踐還不夠,仍需繼續實踐模式。
最后說一句:當開發完功能,跑完UT,你可以放心的上線了的時候,你的UT就成功了。
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.別在再滿屏的 if/ else 了,試試策略模式,真香!!
3.卧槽!Java 中的 xx ≠ null 是什么新語法?
4.Spring Boot 2.5 重磅發布,黑暗模式太炸了!
覺得不錯,別忘了隨手點贊+轉發哦!