1. 程式人生 > >單元測試實踐思考(junit5+jmockit+testcontainer)

單元測試實踐思考(junit5+jmockit+testcontainer)

[TOC] # 背景 之前整理過一篇,基於(SpringCloud+Junit5+Mockito+DataMocker)的框架整理的單元測試。當時的專案是一個編排層的服務專案,所以沒有涉及到資料庫或者其他中介軟體的複雜問題。而且是專案剛開始,程式碼環境不復雜,當時的架構基本上能夠滿足需求。 最近在一個較老專案,現在希望加強專案的程式碼質量,所以開始引入單元測試框架。於是乎先按照原本的設計引入了junit5的整套框架,同時引入了h2用於資料庫模擬,以及rabbitmq的mock服務。這個專案使用的是SpringCloud Alibaba框架,服務註冊和配置管理使用nacos,其他沒有太多特別的地方。但是實際編寫的過程中,發現了一些問題: - Mock框架使用了Mockito和PowerMock,開發人員需要同時使用兩種框架。 - H2的資料庫和實際的Mysql資料庫相比還是有一些差異,比如無法支援函式等情況。 - 單元測試的資料準備相對比較複雜,如何能夠很好的隔離不同單元測試的影響是個問題。 - 單元測試是為了覆蓋率還是為了有強度的質量保證,如何提高研發人員的單元測試質量。 # 方案設計 針對上述問題,我們來一條一條解決。 首先是針對Mock框架,考察之後認為可以選擇Jmockit框架,能夠直接滿足普通方法和靜態方法,但是語法相對不如Mockito自然,學習曲線相對較高。但最終還是決定嘗試以統一框架來做,降低架構的複雜度。 其次是資料庫問題,有兩種方案,一種是完善H2資料庫,可以用自定義的函式來支援缺失的特性,但缺點也很明確,H2始終不是真實的Mysql資料庫。第二種找到了TestContainer方案,這是一個Java操作Docker的類庫,可以利用Java程式碼直接生成Docker的映象與容器並且執行,這樣就有辦法直接啟動一個Mysql的容器用於單元測試,結束後直接完全銷燬。這種方法的缺點在於環境問題,所有需要執行單元測試的環境都需要安裝Docker支援,包含研發自己和CI環境。但是好處在於一個通用的中介軟體模擬方案,後續Redis、MQ或者其他的中介軟體都完全可以使用這樣的方案來模擬了。 資料準備,這個問題我們設定了兩種資料準備的方式。第一部分是在初始化資料庫的時候,匯入基礎指令碼,這部分的指令碼包含結構和資料,是公用的內容所有的單元測試都需要依賴的基礎資料,比如公司、部門、員工、角色、許可權等等。第二部分是在單元測試單個類初始化時,引入資料指令碼,這些資料僅僅是為了單個類/方法中的單元測試使用,執行完方法後會回滾,不會影響到其他單元測試的執行。 最後是單元測試的強度,主要還是一些規範,例如要求所有的單元測試都必須要有斷言,並且斷言的條件是要對資料內容欄位進行合理驗證的。可以參考一下這一篇[寫有價值的單元測試](https://yq.aliyun.com/articles/54478)。 所以最終落定的框架就是 Junit5 + Jmockit + TestContainer。 # 單元測試指導思想 在底層框架搭建之前,可以先討論一下如何才能寫出真正有價值的單元測試,而不是單純為了績效中的單元測試覆蓋率? 之前一段中提到的[寫有價值的單元測試](https://yq.aliyun.com/articles/54478)和阿里Java程式碼規約中有提到一些點 >引用阿里規約: 1. 【強制】好的單元測試必須遵守 AIR原則。 說明:單元測試在線上執行時,感覺像空氣(AIR)一樣並不存在,但在測試質量的保障上, 卻是非常關鍵的。好的單元測試巨集觀上來說,具有自動化、獨立性、可重複執行的特點。 A:Automatic(自動化) I:Independent(獨立性) R:Repeatable(可重複) 2. 【強制】單元測試應該是全自動執行的,並且非互動式的。測試用例通常是被定期執行的,執 行過程必須完全自動化才有意義。輸出結果需要人工檢查的測試不是一個好的單元測試。單元 測試中不準使用 System.out來進行人肉驗證,必須使用 assert來驗證。 3. 【強制】保持單元測試的獨立性。為了保證單元測試穩定可靠且便於維護,單元測試用例之間 決不能互相呼叫,也不能依賴執行的先後次序。 反例:method2需要依賴 method1的執行,將執行結果作為 method2的輸入。 4. 【強制】單元測試是可以重複執行的,不能受到外界環境的影響。 說明:單元測試通常會被放到持續整合中,每次有程式碼 check in時單元測試都會被執行。如 果單測對外部環境(網路、服務、中介軟體等)有依賴,容易導致持續整合機制的不可用。 正例:為了不受外界環境影響,要求設計程式碼時就把 SUT的依賴改成注入,在測試時用 spring 這樣的 DI框架注入一個本地(記憶體)實現或者 Mock實現。 5. 【強制】對於單元測試,要保證測試粒度足夠小,有助於精確定位問題。單測粒度至多是類級 別,一般是方法級別。 說明:只有測試粒度小才能在出錯時儘快定位到出錯位置。單測不負責檢查跨類或者跨系統的 互動邏輯,那是整合測試的領域。 其中有一些思想會決定我們在單元測試程式碼具體的實現方式。我們嘗試了之後,根據上述的指導思想有兩種不同的實現方式。 - 單層隔離 - 內部穿透 接下來我們就兩種方式來進行說明。 ## 單層隔離 正常程式碼分層會分為controller、service、dao等,在單層隔離的思想中,是針對每一層的程式碼做各自的單元測試,不向下穿透。這樣的寫法主要是保證單層的業務邏輯固化且正確。 實踐過程中,例如針對controller層編寫的單元測試需要將對應controller類程式碼檔案外部所有的呼叫全部mock,包括對應的內部/外部的service。其他層的程式碼也是如此。 這樣做的優點: - 單元測試程式碼極其輕量,執行速度快。由於只保證單個類內部的邏輯正確,其他全部mock,所以可以放棄中介軟體的mock,甚至Spring的注入都可以放棄,專注在單元測試邏輯驗證的編寫。這樣整套單元測試程式碼執行完成應該也是輪秒計時,相對來講Spring容器初始化完成可能都需要20秒。 - 真正符合了單元測試的原則,可以在斷網的情況下進行執行。單層邏輯中可以遮蔽服務註冊和配置管理,各種中介軟體的影響。 - 單元測試質量更高。針對單層邏輯的驗證和斷言能夠更加清晰,如果要覆蓋多層,可能會忽略丟失中間的各種驗證環節,如果加上可能條件規模是一個笛卡爾乘積過於複雜。 缺點也是存在: - 單元測試的程式碼量比較大,因為是針對每層單獨編寫單元測試,而且需要mock掉的外部依賴也是比較多的。 - 學習曲線相對較高,由於程式設計師的習慣針對單元測試是給定輸入驗證輸出。所以沒有了底層的輸出,單純驗證過程邏輯要存在一個思維上的轉變。 - 對於低複雜度的專案比較不友好。如果你的專案大部分都是單純的分層之後的CRUD,那單元測試其實可驗證的東西不太多。但是如果是程式碼當中執行了複雜邏輯,這樣的寫法就能夠起到比較好的質量保證。 在這個專案中,最終沒有采用這樣的方法,而是採用了穿透的方式。專案的場景、人員組成、複雜度的實際情況,我覺得用這種方式不算很合適。 ## 內部穿透 穿透,自然就是從頂層一直呼叫到底層。為什麼還要加上內部二字?就是除了專案內的方法可以穿透,專案外部依賴還是要mock掉的。 實踐過程中,就是單元測試針對controller層編寫,但是會完整呼叫service、dao,最終對落地結果進行驗證。 優點: - 程式碼量相對較小,由於進行了穿透所以多層程式碼的覆蓋僅需要從頂層的單元測試驗證即可。 - 學習曲線低,穿透的單元測試更偏向黑盒,開發人員構造輸入條件,然後從落地結果中(儲存,例如資料庫)驗證預期結果。 缺點: - 整體較重,啟動Spring容器,中介軟體mock,整體單元測試執行預計需要是需要分鐘級別。所以基本是要在CI的時候來執行。 # 技術實現 敲定方案之後我們就可以進行技術實現了,這是一個Java專案,使用Maven進行依賴管理。接下來我們主要分為三部分介紹: - 依賴管理 - 基礎架構 - 實現例項 ## 依賴管理 依賴管理中第一個注意的點,由於目前Junit4還佔有較多的市場,我們要儘量去排除掉一些測試相關的依賴中包含對與4的引用。 接下來我先貼出Pom檔案中和單元測試相關的部分 ```xml ``` 依賴的引入基本就是這些了,其中還需要注意的是surefire的外掛配置 ```xml ``` 這裡的注意點是Jmockit需要使用javaagent來初始化JVM引數。 ## 基礎架構 基礎架構的部分,我想分為三點來講: - 單元測試基類,封裝了一些專案使用的基礎Mock物件和公用方法 - 單元測試配置相關 - TestContainer的封裝 其實這三點都是與單元測試基類相關的,分開講各自的實現方式後,最終會給出完整的程式碼。 ### 封裝Junit5&Jmockit 首先是註解的部分Junit4到5註解有調整和變化,而且我們的專案又是基於SpringCloud的,所以最終的單元測試基類BaseTest使用了三個註解 ```java @SpringBootTest(classes = {OaApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional @Slf4j ``` Junit5的類頭部是不需要什麼註解的,主要還是和Spring配合,我們使用了Boot Test提供的SpringBootTest註解,指定了入口的啟動類,為了包含配置檔案,獲取nacos配置。 事務註解是為了讓資料操作方法都能夠回滾,不影響其他單元測試。 最後就是lombok的日誌註解。 接下來就是BeforeAll,AfterAll,BeforeEach,AfterEach幾個註解。 這裡的思路就是使用Jmockit,對待測試業務系統內底層機制進行統一的Mock處理,例如request或者session中的頭部資訊。我這裡的程式碼可能和大家各自的專案中差異比較多,只是提供一個思路。利用Jmockit來Mock我們一些靜態方法獲取物件時,直接返回我們設計的結果物件。 ```java @BeforeAll protected static void beforeAll() { ne