5個編寫技巧,有效提高單元測試實踐

結合單測的實踐,本文總結了幾點單元測試的好處與編寫技巧,希望分享給大家。

1。 什麼是單元測試

“在計算機程式設計中,單元測試又稱為模組測試,是針對程式模組來進行正確性檢驗的測試工作。程式單元是應用的最小可測試部件。在過程化程式設計中,一個單元就是單個程式、函式、過程等;對於面向物件程式設計,最小單元就是方法,包括基類、抽象類、或者派生類中的方法。”

摘錄來自維基百科

單元測試(Unit Testing)顧名思義就是測試一個單元,這裡的單元通常指一個函式或類,區別於整合測試中的模組和系統。整合測試的測試過程通常存在跨系統模組的呼叫,是一種端到端的測試;而單元測試關注物件的顆粒度較小,用來保障一個類或者函式是否按照預期正確的執行。

2。 為什麼要寫單元測試

作為保障程式碼質量的有效手段之一,公司也在積極的推進單元測試。結合單測的實踐,總結了以下幾點單元測試的好處,認真實踐過的同學,應該會有共鳴。

2。1 減少BUG,釋放資源

5個編寫技巧,有效提高單元測試實踐

上面這張圖,旨在說明兩個問題:

85%的缺陷都在程式碼設計階段產生;

發現bug的階段越靠後,耗費成本就越高,呈指數級別的增長。

單元測試是所有測試環節中最底層的一類測試,是第一個環節,也是最重要的一個環節。大多數缺陷是Coding階段引入,修復的成本隨著軟體生命週期進展不斷上升。日常研發中,在交付測試前我們對功能單元進行主流程、各種邊界及異常單元測試的編寫,能有效幫助我們發現程式碼中的缺陷。相對於後期來自測試同學或者線上異常反饋,再來進行排查定位、修復釋出的成本來說,單元測試的價效比是極高的。單元測試可以有效地保障程式碼質量,給我們帶來質量口碑的同時,也為他人和自己減少因修復低階BUG而投入的時間,能夠將精力分配到其他更有意義的事情上。

2。2 為程式碼重構保駕護航

面對專案中歷史遺留的腐化程式碼,我們都有推倒重來的衝動,但它畢竟經過了長時間的穩定性考驗,我們又擔心重構之後出現問題。這是我們經常會遇到的境況,當要重構不是非常熟悉的祖傳程式碼,又沒有充足的測試資源保障的時候,重構引入缺陷的風險還是很大的。

那如何保證重構不出錯呢?Martin Fowler在《重構:改善既有程式碼的設計》提到:

重構是很有價值的工具,但只有重構還不行。要正確地進行重構,前提是得有一套穩固的測試集合,以幫我發現難以避免的疏漏。即便有工具可以幫我自動完成一些重構,很多重構手法依然需要透過測試集合來保障。

除了需要對業務流程有足夠的瞭解並且熟練掌握各種設計思想、模式之外,單元測試是保證重構不出錯的有效手段。當重構完成之後,如果新的程式碼仍然能透過單元測試,那就說明程式碼原有正確的邏輯未被破壞,原有的外部可見行為沒有發生改變。單元測試給了我們重構的信心與底氣。

2。3 既是編寫單測也是CodeReview

單元測試和CR是保障程式碼質量行之有效的兩個手段。在研發交付過程中,通常我們提交CR的時機較為滯後,評審同學指出待最佳化或修復的時間點也較晚,修復的風險和成本上都有所增加。

我們編寫編碼單元測試過程,其實也是自我CodeReview的過程。在這個過程中,我們對功能單元主流程、邊界及異常進行測試,也在自我審視程式碼的規範、邏輯及設計。既提高了後續提交CR的質量與評審效率,也將問題提前暴露。

2。4 便於除錯與驗證

當專案存在多個協同方時,我們只需按照約定mock出依賴項的資料,無需等所有依賴的應用介面開發部署完成後再進行除錯,提高了我們協同的效率與質量。我們將功能需求進行拆解,在開發完每一個小功能點時,即可進行單元測試的編寫與驗證,這種習慣能讓我們對編碼得到快速的驗證反饋;同時,在開發完整個功能時,我們需要跑一遍專案所有的單測用例,可以清晰的感知,本次整個功能需求的改動是否對已有業務case造成影響。

如果我們能夠保障每個類、函式都能透過單元測試按照預期業務邏輯執行,那整合後的功能模組或系統,出問題的機率都能大大降低。從這個意義上講,單元測試也對整合測試、系統測試做了有力的支撐。

2。5 驅動設計與重構

設計和編碼的時候,我們很難將所有的問題都想清楚。那我們知道,評判程式碼質量重要的的標準之一就是程式碼的可測性。如果對一段程式碼進行單測,發現難於編寫,需要編寫的case非常多,或者當前的測試框架無法mock依賴物件,需要依賴其他具備高階特性的測試框架時,我們需要回過頭來審視程式碼,是否編碼設計得不合理,導致程式碼的可測性不高。這是個正反饋的過程,讓我們有針對性的進行重新設計與重構。

3。 怎樣編寫單元測試

3。1 單元測試框架的構建

3。1。1 單元測試框架JUnit

JUnit是目前Java語言應用最為廣泛的單元測試框架,用於編寫和執行可重複的自動化測試,它包含以下特性:

用於測試期望結果的斷言(Assertion)

用於共享共同測試資料的測試工具

用於方便的組織和執行測試的測試套件

圖形和文字的測試執行器

多數Java的開發環境都已經集成了JUnit作為單元測試的工具,開源框架對JUnit 都有相應的支援

3。1。2 單元測試Mock框架

專案中依賴關係往往往非常複雜,單元測試Mock框架做的事就是模擬被測試類的依賴項,提供預期的行為和狀態,使得我們的單測可以聚焦在被測試類本身,而不必受到依賴項的複雜度的影響。

這裡我們討論常用的Mockito與

PowerMock,兩者都是作為單元測試模擬框架,模擬應用中複雜的依賴物件。Mockito基於動態代理的方式實現,PowerMock在

Mockito基礎上增加了

類載入器以及位元組碼篡改技術,使

其可以實現完成對private/static/final方法的Mock。

公司使用JaCoCo來做單元覆蓋率的檢測,當我們使用支援位元組碼篡改的mock工具的時候,可能會造成:

測試失敗,mock工具與jacoco同時修改位元組碼時引入的衝突

某些類的覆蓋率為0

所以我們推薦使用Mockito來作為我們的單元測試Mock框架,原因有二:

在版本3。4。0以後,Mockito支援靜態方法的mock。並且作為SpringBootTest預設整合的Mock工具,所以建議大家使用高版本的Mockito,並透過它來完成靜態方法的Mock

不提倡使用PowerMock,並不是一味追求單測覆蓋率,而是當我們需要使用到具備高階特性mock工具時,我們需要審視程式碼的合理性,並嘗試進行最佳化重構,使其具備較好的可測性

3。1。3 依賴引入

3。1。3。1 新增JUnit的maven依賴

Springboot專案

org。springframework。boot spring-boot-starter-test test

SpringMVC專案

junit junit 4。12 test

3。1。3。2 單測Mock框架的引入

org。mockito mockito-core 4。7。0 test org。mockito mockito-inline 4。7。0 test

3。2 單測方法的命名

3。2。1 單元測試類的規範

單元測試類需要放在工程的test目錄下,比如xxx/src/test/java

單測類的命名按照規範,應以被測類名開頭,並追加Test作為結尾,比如ContentService -> ContentServiceTest

3。2。2 單元測試方法規範

3。2。2。1 測試方法的命名

好的單元測試方法名,能讓我們快速知道測試的場景、意圖及驗證的預期。

建議採用should_{預期結果}_when_{被測方法}_given_{給定場景}

舉個

@Testpublic void should_returnFalse_when_deleteContent_given_invokeFailed() { 。。。}

反例

@Testpublic void testDeleteContent() { 。。。}

3。2。2。2 單測方法實現分層

單測方法的實現如果分層清晰,能讓程式碼便於理解,一目瞭然,同時也能提高後續的CR的效率

這裡我們建議採用given-when-then的三段落結構

舉個

@Testpublic void should_returnFalse_when_deleteContent_given_invokeFailed() { // given Result deleteDocResult = new Result<>(); deleteDocResult。setEntity(Boolean。FALSE); when(docManageService。deleteContentDoc(anyLong()))。thenReturn(deleteDocResult); when(docManageService。queryContentDoc(anyLong()))。thenReturn(new DocEntity()); // when Long contentId = 123L; Boolean result = contentService。deleteContent(contentId); // then verify(docManageService, times(1))。queryContentDoc(contentId); verify(docManageService, times(1))。deleteContentDoc(contentId); Assert。assertFalse(result);}

3。3 單測方法的示例

3。3。1 程式碼案例

public class SnsFeedsShareServiceImpl { private SnsFeedsShareHandler snsFeedsShareHandler; @Autowired public void setSnsFeedsShareHandler(SnsFeedsShareHandler snsFeedsShareHandler) { this。snsFeedsShareHandler = snsFeedsShareHandler; } public Result shareFeeds(Long feedsId, String platform, List snsAccountList) { if (!validateParams(feedsId, platform, snsAccountList)) { return ResponseBuilder。paramError(); } try { Result snsResult = snsFeedsShareHandler。batchShareFeeds(feedsId, platform, snsAccountList); if (Objects。isNull(snsResult) || !snsResult。isSuccess() || Objects。isNull(snsResult。getModel())) { return ResponseBuilder。buildError(ResponseEnum。SNS_SHARE_SERVICE_ERROR); } return ResponseBuilder。successResult(snsResult。getModel()); } catch (Exception e) { LOGGER。error(“shareFeeds error, feedsId:{}, platform:{}, snsAccountList:{}”, feedsId, platform, JSON。toJSONString(snsAccountList), e); return ResponseBuilder。systemError(); } } // 省略程式碼。。。}

3。3。2 單元測試程式碼案例

@RunWith(MockitoJUnitRunner。class)public class SnsFeedsShareServiceImplTest { @Mock SnsFeedsShareHandler snsFeedsShareHandler; @InjectMocks SnsFeedsShareServiceImpl snsFeedsShareServiceImpl; @Test public void should_returnServiceError_when_shareFeeds_given_invokeFailed() { // given Result invokeResult = new Result<>(); invokeResult。setSuccess(Boolean。FALSE); invokeResult。setModel(Boolean。FALSE); when(snsFeedsShareHandler。batchShareFeeds(anyLong(), anyString(), anyList()))。thenReturn(invokeResult); // when Long feedsId = 123L; String platform = “TEST_SNS_PLATFORM”; List snsAccountList = Collections。singletonList(“TEST_SNS_ACCOUNT”); Result> result = snsFeedsShareServiceImpl。shareFeeds(feedsId, platform, snsAccountList); // then verify(snsFeedsShareHandler, times(1))。batchShareFeeds(feedsId, platform, snsAccountList); Assert。assertNotNull(result); Assert。assertEquals(result。getResponseCode(), ResponseEnum。SNS_SHARE_SERVICE_ERROR。getResponseCode()); } }

3。4 單測的編碼技巧

3。4。1 Mock依賴物件

@RunWith(MockitoJUnitRunner。class)public class ContentServiceTest { @Mock DocManageService docManageService; @InjectMocks ContentService contentService; 。。。}

MockitoJUnitRunner使Mockito的註解生效或者使用初始化方法MockitoAnnotations。initMocks(this)

利用@Mock模擬各種依賴物件

使用@InjectMocks將mock出的依賴物件注入到目標測試物件中。以上述程式碼為例,單測中將docManageService注入到contentService

當然我們也可以使用直接初始化或者@Spy的方式來模擬物件,然後使用Setter方法來進行模擬物件的注入,這裡介紹了較為簡便的方式。

點選檢視原文,獲取更多福利!

https://developer。aliyun。com/article/1081898?utm_content=g_1000365270

版權宣告:本文內容由阿里雲實名註冊使用者自發貢獻,版權歸原作者所有,阿里雲開發者社群不擁有其著作權,亦不承擔相應法律責任。具體規則請檢視《阿里雲開發者社群使用者服務協議》和《阿里雲開發者社群智慧財產權保護指引》。如果您發現本社群中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社群將立刻刪除涉嫌侵權內容。