Java Springboot 后端使用Mockito庫(kù)進(jìn)行單元測(cè)試流程分析
1 為什么要使用mock進(jìn)行單元測(cè)試
如果使用SpringbootTest進(jìn)行單元測(cè)試,那么將啟動(dòng)spring服務(wù),生成容器和bean并加載所有外部資源或依賴,當(dāng)工程量很大時(shí)啟動(dòng)非常費(fèi)時(shí),顯然為了一個(gè)小小的單元測(cè)試啟動(dòng)整個(gè)項(xiàng)目有一些大動(dòng)干戈。
此外,有時(shí)部分外部依賴無(wú)法獲?。ㄈ鐢?shù)據(jù)庫(kù)連接、緩存或接口獲取有問(wèn)題),而單元測(cè)試旨在測(cè)試某個(gè)類具體的方法,測(cè)試的是該方法的功能邏輯,不應(yīng)關(guān)注其依賴的對(duì)象,以防測(cè)試時(shí)各級(jí)方法互相調(diào)用太深(調(diào)用的方法應(yīng)該在它自己的單元測(cè)試中進(jìn)行測(cè)試),相當(dāng)于只關(guān)注測(cè)試方法的寬度而不在意它的深度。
因此,對(duì)于要測(cè)試的方法中使用到的外部類或方法,可以考慮使用模擬對(duì)象來(lái)模擬它們的調(diào)用并自定義好返回的值,這樣能夠在不執(zhí)行真實(shí)調(diào)用方法的同時(shí)讓單元測(cè)試正常進(jìn)行,這就是mock的原理。使用mock可以很好的將要測(cè)試的方法和它的依賴分隔開(kāi),降低了模塊間的耦合,非常適合外部資源較難構(gòu)造或方法調(diào)用太深的場(chǎng)景。
2 使用mock的注意點(diǎn)
- 通過(guò)mock進(jìn)行單元測(cè)試時(shí)并未啟動(dòng)spring,那么不會(huì)加載容器并生成bean,就無(wú)法通過(guò)自動(dòng)注入的方式獲取依賴,需要使用mock注解
- 如果測(cè)試的相關(guān)功能與spring有關(guān)(比如AOP),那么還是需要使用SpringbootTest
- 啟用mock時(shí)可以看成啟動(dòng)了普通的java項(xiàng)目,在普通項(xiàng)目中能運(yùn)行的代碼在mock中也能運(yùn)行
- 單元測(cè)試時(shí)并非所有調(diào)用的外部方法或使用到的外部類都需要mock,如實(shí)體類可以直接new,工具類方法可以直接運(yùn)行(不存在外部依賴的工具類,比如用來(lái)進(jìn)行數(shù)據(jù)格式處理的工具類,通常調(diào)用其靜態(tài)方法),不需要使用mock
3 mock使用流程
3.1 測(cè)試前配置
使用mock首先需要在pom文件中導(dǎo)入相關(guān)依賴,然后可以使用@Mock
或Mockito庫(kù)的mock方法來(lái)生成模擬對(duì)象,如果使用@Mock
則需要先添加以下注解或代碼:
- 在測(cè)試類前添加
@RunWith(MockitoJUnitRunner.class)
或@ExtendWith(MockitoExtension.class)
- 在測(cè)試類內(nèi)添加如下方法:
//測(cè)試前準(zhǔn)備,可以在這里進(jìn)行一些基礎(chǔ)配置 @BeforeEach void setUp(){ MockitoAnnotations.openMocks(this); //開(kāi)啟mock,搭配@Mock使用 }
3.2 注入待測(cè)試類并模擬其中使用的變量
若需要測(cè)試的類為UserService
,那么在它的測(cè)試類UserServiceTest
中需要使用注解@InjectMocks
注入UserService
的實(shí)例,該注解的作用是自動(dòng)創(chuàng)建并注入一個(gè)類的實(shí)例,同時(shí)將標(biāo)記為@Mock
的依賴注入到該實(shí)例中,注意@InjectMocks
只能注入class。
@InjectMocks UserService userService;
3.2.1 模擬成員變量
待測(cè)試類的成員變量需要使用@Mock
注解注入,無(wú)法通過(guò)Mockito.mock()
方法生成模擬對(duì)象,mock()方法生成的成員模擬對(duì)象在執(zhí)行時(shí)會(huì)報(bào)空指針錯(cuò)誤,并未真正生成模擬對(duì)象。
若UserService
中有成員變量UserMapper
,那么使用如下代碼注入:
@Mock UserMapper
簡(jiǎn)單來(lái)說(shuō)就是將UserService
里面的所有成員變量直接復(fù)制到Test
里,然后將@Autowired
改成@Mock
即可。
3.2.2 模擬靜態(tài)對(duì)象
當(dāng)代碼中需要調(diào)用某個(gè)類的靜態(tài)方法時(shí),需要使用mockStatic()
方法生成一個(gè)模擬靜態(tài)對(duì)象,如被測(cè)方法中若存在如下代碼:
DictUtil.getIns("A_END"); //DictUtil類中存在一些外部依賴,因此需要模擬
那么首先需要生成DictUtil
的模擬靜態(tài)對(duì)象:
mockStatic(DictUtil.class); //上下兩種寫法都可以 MockedStatic<DictUtil> dictUtilMockedStatic = mockStatic(DictUtil.class);
3.2.3 模擬普通變量
對(duì)于代碼中生成的一些中間變量,而在后期又需要使用這些變量進(jìn)行方法調(diào)用時(shí),需要先使用mock將中間變量模擬出,如被測(cè)方法中若存在代碼:
UserCache.getInstance().getName("12345");
那么需要先模擬出UserCache
的一個(gè)實(shí)例(打樁時(shí)不能對(duì)連續(xù)調(diào)用的方法同時(shí)打樁,需先生成中間變量),此時(shí)可以使用@Mock
或者mock方法:
// 1. 當(dāng)成測(cè)試類成員變量注入 @Mock UserCache userCache; // 2. 在測(cè)試方法中直接模擬 UserCache userCache = Mockito.mock(UserCache.class);
3.3 打樁模擬方法調(diào)用行為
3.3.1 非靜態(tài)方法打樁
打樁即對(duì)調(diào)用的方法模擬傳參并設(shè)置返回值,參數(shù)個(gè)數(shù)和返回值需要與原方法對(duì)應(yīng)。打樁之后不會(huì)真正去調(diào)用,會(huì)忽略真實(shí)方法的執(zhí)行結(jié)果,一般使用Mockito.when().thenReturn()
進(jìn)行打樁。
若被測(cè)類方法中存在代碼:
User user = UserMapper.selectById("0000");
那么測(cè)試方法中的打樁可以是:
//實(shí)體類不需要mock,可以直接生成,然后使用setter方法賦值 User user = new User(); // 1. 固定了打樁參數(shù)為“0000”,源代碼調(diào)用中參數(shù)為非“0000”時(shí)打樁失敗 Mockito.when(UserMapper.selectById("0000")).thenReturn(user) // 2. 為了代碼的健壯性,通常使用Mockito.anyxxx()方法來(lái)接收參數(shù),根據(jù)xxx的不同來(lái)指定不同的類型,只要是該類型的任意參數(shù)都可以打樁 Mockito.when(UserMapper.selectById(Mockito.anyString())).thenReturn(user)
3.3.2 靜態(tài)方法打樁
以原代碼UserCache.getInstance().getName("12345");
為例,靜態(tài)方法打樁有兩種寫法:
1) 普通寫法
UserCache userCache = Mockito.mock(UserCache.class); mockStatic(UserCache.class); Mockito.when(UserCache.getInstance()).thenReturn(userCache); Mockito.when(userCache.getName(Mockito.anyString())).thenReturn("TestName");
2) lambda寫法-無(wú)參靜態(tài)方法調(diào)用
UserCache userCache = Mockito.mock(UserCache.class); MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class); //1. 當(dāng)靜態(tài)方法無(wú)參時(shí),可以使用::進(jìn)行方法調(diào)用 userCacheMockedStatic.when(UserCache::getInstance).thenReturn(userCache); //2. 也可以使用lambda寫法,使用.進(jìn)行調(diào)用 userCacheMockedStatic.when(() -> UserCache.getInstance()).thenReturn(userCache); //3. 不能使用如下寫法: //userCacheMockedStatic.when(UserCache.getInstance()).thenReturn(userCache); Mockito.when(userCache.getName(Mockito.anyString())).thenReturn("TestName");
3) lambda寫法-有參靜態(tài)方法調(diào)用
假如上述getInstance
方法有一個(gè)int參數(shù),那么可以通過(guò)1)中的普通寫法進(jìn)行打樁,或者使用2)-2的方式進(jìn)行打樁,然后使用Mockito.anyString()
傳參,但是不能使用2)-1或2)-3的寫法。
3.3.3 Maven test靜態(tài)模擬報(bào)錯(cuò)問(wèn)題
當(dāng)單獨(dú)運(yùn)行一個(gè)Test時(shí)沒(méi)有問(wèn)題,但是當(dāng)集體執(zhí)行Test時(shí)會(huì)出現(xiàn)如下報(bào)錯(cuò):
‘static mocking is already registered in the current thread To create a new mock, the existing static mock registration must be deregistered’
這是因?yàn)橛袃蓚€(gè)以上的測(cè)試方法都使用了同一個(gè)靜態(tài)mock對(duì)象,如兩個(gè)Test中都有MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class)
,而靜態(tài)mock對(duì)象不允許公用,因此需要在每個(gè)靜態(tài)mock對(duì)象執(zhí)行后對(duì)其進(jìn)行關(guān)閉,下一個(gè)方法才能繼續(xù)使用它,故需要在每個(gè)Test末尾中寫:userCacheMockedStatic.close()
。
由于MockedStatic<T>
的父接口繼承了AutoCloseable
接口,因此可以使用 try-resources語(yǔ)句實(shí)現(xiàn)資源的自動(dòng)關(guān)閉:
try( // mockStatic(UserCache.class); //java8不支持這種寫法 MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class); // ……其他靜態(tài)模擬對(duì)象聲明 ){ //其他測(cè)試代碼 }
也可以選擇用try-catch-finally在finally語(yǔ)句里手動(dòng)關(guān)閉:
try( MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class); // ……其他測(cè)試代碼 )finally{ userCacheMockedStatic.close(); }
3.4 斷言判斷測(cè)試結(jié)果
要檢測(cè)測(cè)試的結(jié)果,需要使用斷言Assertions
進(jìn)行判斷,根據(jù)實(shí)際測(cè)試要求來(lái):
User user = UserService.getById("123"); User expectUsr = new User("123"); Assertions.assertEquals(expectUsr ,user)://判斷測(cè)試結(jié)果user與期望的對(duì)象是否一致 Assertions.assertNotNull(user)://判斷測(cè)試結(jié)果user是否非空
到此這篇關(guān)于Java Springboot 后端使用Mockito庫(kù)進(jìn)行單元測(cè)試流程的文章就介紹到這了,更多相關(guān)Springboot Mockito單元測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解如何更改SpringBoot TomCat運(yùn)行方式
這篇文章主要介紹了詳解如何更改SpringBoot TomCat運(yùn)行方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Spring Cloud Gateway 默認(rèn)的filter功能和執(zhí)行順序介紹
這篇文章主要介紹了Spring Cloud Gateway 默認(rèn)的filter功能和執(zhí)行順序,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10SpringBoot2.3新特性優(yōu)雅停機(jī)詳解
這篇文章主要介紹了SpringBoot2.3新特性優(yōu)雅停機(jī)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05