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