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