Springboot單元測試編寫實踐
1 前言
在日常的開發(fā)過程中,為了提高代碼的可靠性和健壯性,同時也是檢測代碼的質(zhì)量,減少測試環(huán)節(jié)的問題,會對完成的業(yè)務功能代碼編寫單元測試。有時間單元測試的覆蓋率也是工作的一部分。作者最近被安排了一項艱巨的單測任務,在本文中,將分享一些單元測試的實踐和心得。
2 生成單元測試
通常情況下,單元測試都是使用 junit 編寫的,但是這種方式會真實的調(diào)用數(shù)據(jù),如何優(yōu)雅的實現(xiàn)單元測試是一個問題。這里使用的是 powermock
來實現(xiàn)測試用例的編寫。引入 powermock
依賴如下所示:
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.11.2</version> <scope>test</scope> </dependency>
在編寫單元測試之前,需要在 idea
中安裝一個 squaretest
插件,在需要編寫單元測試的類中,通過右鍵 generate -> generate Test
可以打開一個面板,可以選擇一個模板,就可以在test
中生成具體的單元測試類。
可以這樣說,有了這個便捷的插件助力,對于簡單的場景,就可以完全覆蓋其業(yè)務功能。
3 單測重點
雖然說通過 squaretest
可以實現(xiàn)大部分代碼的編寫,但是有一些場景還不能那么智能的實現(xiàn),需要編寫代碼實現(xiàn)功能。如下圖所示,除了第三個單測點外,其它的都是不常見的類型。
3.1 mock static 方法
static 方法的 mock
需要使用到 mockStatic
方法,具體的操作如下所示:
// 第一步需要在測試類上添加類名稱 @PrepareForTest(value = {類名.class}) // 第二步需要使用 mockStatic 聲明,然后進行mock 操作 mockStatic(類名.class) when(類名.static方法()).thenReturn(mock結果);
static
方法的 mock
, 適用于需要加載系統(tǒng)配置或者初始化文件的場景,通過 mock
類的 static
方法,即可獲取相應的返回值,避免類的初始化導致單元測試報錯。
3.2 mock 分布式鎖
分布式鎖的 mock, 需要考慮的方面比較多,首先是根據(jù) redisclient 獲取分布式鎖,然后調(diào)用 tryLock 并等待加鎖的返回信息,這里其實是需要兩個 mock, 但是 getLock 返回的 RLock 是一個接口,不能使用創(chuàng)建新類的方式來實現(xiàn),這里就需要使用 mock() 來創(chuàng)建一個 RLock, 然后在其基礎上進行 mock , 由此可以實現(xiàn)兩層的 mock。這里需要說明的是,分布式鎖使用的 Redisson 來實現(xiàn)的。
// mock RLock RLock mock = mock(RLock.class); // mock tryLock 和 getLock 兩個方法 when(mock.tryLock(anyLong(), eq(TimeUnit.MINUTES))).thenReturn(Boolean.TRUE); when(mockRedisUtils.getLock(anyString())).thenReturn(mock);
3.3 mock spring 中的 bean
在單元測試中常用的 mock
即測試類中注入的 service、business、mapper
等內(nèi)容,通過 mock
所涉及的方法,以期得到對應的返回值繼續(xù)業(yè)務流程的繼續(xù)。 when 和 thenReturn 需要組合使用,根據(jù)傳入的方法參數(shù)返回預期值。方法的入?yún)⒖梢允?any(), any(類.class), anyString(), anyInt(), anyLong()
等,但需要注意的是傳入的參數(shù)不能為 null
,如果需要精確匹配,則需要使用 eq()
。此外, thenReturn
的返回值可以有多個,支持鏈式調(diào)用,如果返回值有多個,則表示第一次調(diào)用方法返回第一個值,第二次調(diào)用方法返回第二個值,以此類推。另外,還有模擬方法調(diào)用發(fā)送異常的場景,則使用 doThrow
來返回對應的異常。
// mock 業(yè)務查詢和操作 when(mockMapper.selectByUserId(eq("123"))).thenReturn(user); when(mockMapper.selectByUserId(anyString())).thenReturn(user, user, user); when(mockMapper.selectByUserId(anyString())).thenReturn(user).thenReturn(user).thenReturn(user); when(mockMapper.updateById(any(User.class))).thenReturn(1); // 模擬調(diào)用拋出異常 doThrow(RuntimeException.class).when(mockUserMapper).selectByUserId(anyString());
3.4 mock redis template 操作
對于 redis
的操作,其實和分布式鎖的操作類似,以操作字符串類型為例,需要先獲取一個 opsForValue
而后來進行操作 redisTemplate.opsForValue().方法
,由于 ValueOperations
也是一個接口,所以需要使用 mock
來獲取一個操作對象,基于此在進行 mock
所涉及的方法。
ValueOperations operations = mock(ValueOperations.class); when(operations.increment(anyString())).thenReturn(230L); when(redisTemplate.opsForValue()).thenReturn(operations);
3.5 mock 事務 transactionTemplate
事務的操作是在特殊業(yè)務場景下才會用到,這里的只是借此場景來說明如何對匿名類中的方法進行單測。默認情況下生成的測試代碼是不具備這種能力的,需要使用 thenAnswer
來處理。以下列舉了兩種方式,一種是匿名類的方式,一種是 lambda
表達式的方法。根據(jù)以下方式操作,功能內(nèi)部類中代碼可以實現(xiàn)覆蓋。如果項目中使用了線程池,也同樣可以依據(jù)此方法處理。
// mock transaction when(mockTransaction.execute(any(TransactionCallback.class))).thenReturn(true); // 匿名內(nèi)部類 when(mockTransaction.execute(Mockito.<TransactionCallback>any())).thenAnswer(new Answer<Object>() { public Object answer(InvocationOnMock invocation) { Object[] args = invocation.getArguments(); TransactionCallback arg = (TransactionCallback) args[0]; return arg.doInTransaction(new SimpleTransactionStatus()); } }); // lambda 方式 Answer<Object> answer = invocation -> { Object[] args = invocation.getArguments(); TransactionCallback arg = (TransactionCallback) args[0]; return arg.doInTransaction(new SimpleTransactionStatus()); }; when(mockTransaction.execute(any())).thenAnswer(answer);
3.6 mock http ResetTemplate
通常情況下 http
的調(diào)用是使用 httpUtils
工具類, 但是特殊的情況下使用 restTemplate
進行調(diào)用,如下所示,可以實現(xiàn)對 http
調(diào)用的 mock
,這里只是一種 post
的調(diào)用方式,如果有其他的類型調(diào)用可以參考編寫 mock
。
// mock http reset http JSONObject body = new JSONObject(); body.put("code", "0000"); when(restTemplate.postForObject(anyString(), any(HttpEntity.class), eq(JSONObject.class))).thenReturn(body);
3.7 斷言
在編寫完成單元測試后,需要對結果進行斷言,通常情況下每個方法都需要有一個斷言,針對 void
方法,可以使用 verify
來進行處理,校驗方法中的某一個環(huán)節(jié)是否被處理過?,F(xiàn)在項目的集成與發(fā)版都實現(xiàn)了自動化,沒有斷言或者 verify
的方法會掃描出存在漏洞。斷言可以分為返回對象不為空或者返回值和預期值相同與否。verify
可以添加 times(1)
進行測試,校驗其方法調(diào)用的次數(shù)。
// 斷言返回對象不為空 Assert.assertNotNull(result); // 斷言結果的期望值和結果值相同 Assert.assertEquals(result, expectedResult); // 斷言方法中的某個環(huán)節(jié)被執(zhí)行過 調(diào)用一次 // times(n) 調(diào)用 n 次 // never() 沒有調(diào)用,相當于 調(diào)用 0 次 times(0) // atMostOnce() 最多調(diào)用一次 // atLeastOnce() 最少調(diào)用一次 // atLeast() 最少一次 // atMost() 最多一次 verify(mockUserMapper, times(2)).selectByUserId(anyString()); verify(mockUserMapper, atLeast(1)).selectByUserId(anyString());
3.8 異常用例
以上講述的都是正常的單測,在實際的業(yè)務中還要模擬一些異常的場景,所以需要異常用例的編寫也是需要的,這樣進入到異常場景也可以提高單測的覆蓋率。
// 單測期望拋出一個異常信息 @Test(expected = RuntimeException.class) public void testMockTest_TransactionTemplateThrowsTransactionException() throws Exception { ... // 異常操作 when(mockTransaction.execute(any(TransactionCallback.class))).thenThrow(RuntimeException.class); .... }
3.9 測試類的 setUp
通常情況下,在復雜的業(yè)務場景,需要對測試類設置屬性值,一般情況下屬性值都是從配置文件讀取,那怎么對其設置屬性值呢?這里用到了反射的知識,通過 hutool 工具類,可以對類的某個屬性賦值。同時也可以在這里做一些初始化的操作或者測試類單測前的準備工作。
@Before public void setUp() { initMocks(this); // 使用反射的方式設置對象屬性的值 ReflectionTestUtils.setField(mockBusinessUnderTest, "name", "test"); }
4 總結
在本文中,主要介紹了編寫單元測試的實踐,通過 squaretest
插件可以解決大部分的測試場景,如果有測試覆蓋不到的地方,無外乎以上介紹的幾種特殊的場景。掌握了以上的方式,可以很輕松的將單測覆蓋率提高到一個比較高的水平。
以上就是Springboot單元測試編寫實踐的詳細內(nèi)容,更多關于Springboot單元測試的資料請關注腳本之家其它相關文章!
相關文章
Maven添加Tomcat插件實現(xiàn)熱部署代碼實例
這篇文章主要介紹了Maven添加Tomcat插件實現(xiàn)熱部署代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-04-04SpringBoot如何使用mail實現(xiàn)登錄郵箱驗證
在實際的開發(fā)當中,不少的場景中需要我們使用更加安全的認證方式,同時也為了防止一些用戶惡意注冊,我們可能會需要用戶使用一些可以證明個人身份的注冊方式,如短信驗證、郵箱驗證等,這篇文章主要介紹了SpringBoot如何使用mail實現(xiàn)登錄郵箱驗證,需要的朋友可以參考下2024-06-06springboot接收json數(shù)據(jù)時,接收到空值問題
這篇文章主要介紹了springboot接收json數(shù)據(jù)時,接收到空值問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05SpringCloud讓微服務實現(xiàn)指定程序調(diào)用
這篇文章主要介紹了SpringCloud讓微服務實現(xiàn)指定程序調(diào)用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06