概述
我們在開發過程中,爲了代碼的穩定性也好,爲了少給自己以後的開發挖坑也好,多寫單元測試絕對是一件性價比超高的繁瑣事,既然是繁瑣的事情,我想大部分人是不願意寫的,我也不願意寫,但是要做好一個程序員,不僅僅的去做一個低級碼農,那就從最簡單的地方做起,單元測試就是一件特別簡單的事。寫了單元測試的代碼的健壯性和邏輯性絕對要更上一個層次,而且對於開發而言理解回顧代碼邏輯是一件必不可少的事情
一個 bug 被隱藏的時間越長,修復這個 bug 的代價就越大。前期多去寫一些邊界測試,後期就有時間學習,開發時間和迴歸回顧時間的比例應該是1:1,單元測試是一個方法層面上的測試,也是最細粒度的測試。用於測試一個類的每一個方法都已經滿足了方法的功能要求,可以避免測試點的遺漏,爲了更好的進行測試,可以提高測試效率。在開發中,對於自己開發的模塊,只有在通過單元測試之後,才能提交到 SVN 庫 或者 Git 庫。
工具
我將在下面介紹下PowerMockRunner和SpringRunner兩個單元測試的運行環境。
SpringRunner
SpringRunner 繼承了SpringJUnit4ClassRunner,SpringRunner是SpringJUnit4ClassRunner的一個別名,沒有擴展任何功能。下面我們來看下示例
package com.hly.unitest.controller;
import com.alibaba.fastjson.JSON;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceNoMockTest {
@Autowired
private UserService userService;
@Test
public void getUser() throws Exception {
UserInfo result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
System.out.println("the 1st time: result = " + JSON.toJSONString(result));
}
}
返回顯示:
分析:
首先我們分析下註解:
@RunWith: 用於指定junit運行環境,是junit提供給其他框架測試環境接口擴展,爲了便於使用spring的依賴注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作爲JUnit測試環境。在JUnit中有很多個Runner,他們負責調用你的測試代碼,每一個Runner都有各自的特殊功能,你要根據需要選擇不同的Runner來運行你的測試代碼。我們此篇文章只探討SpringRunner和PowerMockRunner。
@SpringBootTest: 替代了spring-test中的@ContextConfiguration註解,目的是加載ApplicationContext,啓動spring容器@SpringBootTest註解會自動檢索程序的配置文件,檢索順序是從當前包開始,逐級向上查找被@SpringBootApplication或@SpringBootConfiguration註解的類。
提出問題: 這樣的測試方式會連接數據庫,進行網絡請求等等耗時操作,甚至數據庫中的準備數據是個非常麻煩的事情,各個邏輯分支如果都需要測試,這在大型的項目中工作量是不可想象的,所以有沒有可能對數據庫這種操作直接不處理,這需要mock能力,爲此引入了MockBean,下面來看示例:
package com.hly.unitest.controller;
import com.alibaba.fastjson.JSON;
import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import static org.mockito.ArgumentMatchers.any;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserInfoMapper userInfoMapper;
@Test
public void getUser() {
UserInfo result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
System.out.println("the 1st time: result = " + JSON.toJSONString(result));
UserInfo userInfo = mockUserInfo();
Mockito.when(userInfoMapper.selectByUserId(any())).thenReturn(userInfo);
result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
System.out.println("the 2nd time: result = " + JSON.toJSONString(result));
}
private UserInfo mockUserInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setUserId("654321");
userInfo.setUserName("B");
userInfo.setPassword("pwB");
userInfo.setGender("女");
return userInfo;
}
}
返回顯示:
分析:
我們在UserInfoMapper上加了註解@MockBean,這個註解的意思所有的UserInfoMapper的方法都被mock,返回都將變成null,所以第一次返回值爲null;我們在測試方法中藉助Mockito.when將userInfoMapper.selectByUserId的返回值進行自定義,這樣就完成了對數據庫的返回對象的控制,不用怕數據庫的更改導致測試用例的分支沒有跑完成的問題出現。
提出問題: 我們發現這樣的處理雖然符合了上個問題的預期,但是又有了新的問題出現,第一:既然將數據庫和網絡操作全部mock,那好像就沒有必要啓動Spring,@MockBean有個特別大的危害就是導致頻繁的啓動Spring,而spring boot 的啓動時間比較耗時,所以@MockBean,很有可能導致測試運行的很慢;第二:如果有這樣一個方法,是UserService 的私有方法,裏面特別複雜的操作,我並不想在這個測試用例中進行測試,可以不可以mock,或者有一個靜態類,類中的方法有沒有方式mock,還有UserService的變量我又沒有方式可以直接賦值等等,這些SpringRunner也就是SpringJUnit4ClassRunner都是沒有辦法解決的,這需要下面的有個JUNIT工具解決,也就是PowerMockRunner。
PowerMockRunner
PowerMock基本上cover了所有Mockito不能支持的case(大多數情況也就是靜態方法,但其實也可以支持私有方法和構造函數的調用)。PowerMock使用了字節碼操作,因此它是自帶Junit runner的。在使用PowerMock時,必須使用@PrepareForTest註釋被測類,mock纔會被執行。下面我們來看示例:
對UserService進行單元測試:
package com.hly.unitest.service;
import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.model.UserInfoRequest;
import com.hly.unitest.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class UserService {
private String finalStr = "finalStr";
@Autowired
private UserInfoMapper userInfoMapper;
public void save(UserInfoRequest request) {
UserInfo userInfo = new UserInfo();
userInfo.setUserId(request.getUserId());
userInfo.setUserName(request.getUserName());
userInfo.setPassword(request.getPassword());
userInfo.setGender(request.getGender());
Date currDate = CommonUtil.getMaxDayOfThisMonth(new Date());
userInfo.setCreateTime(currDate);
userInfo.setUpdateTime(currDate);
userInfoMapper.insertSelective(userInfo);
}
public UserInfo getUserInfo(String userId) {
if (CommonUtil.isEmpty(userId)) {
return null;
}
String assetStr = getAssetStr0("a", "b");
if (!"un".equals(assetStr)){
System.out.println("assetStr = " + assetStr);
return null;
}
getAssetNull();
UserInfo userInfo = userInfoMapper.selectByUserId(userId);
return userInfo;
}
public UserInfo getUser(String userId) {
UserInfo userInfo = userInfoMapper.selectByUserId(userId);
return userInfo;
}
public String testStaticMethod() {
String randomStr = CommonUtil.generateUUID();
return randomStr;
}
public String testClassVariables() {
return finalStr;
}
private String getAssetStr() {
System.out.println("getAssetStr");
return "unit-test";
}
private String getAssetStr0(String a, String b) {
System.out.println("getAssetStr0");
return "unit-test";
}
private void getAssetNull() {
System.out.println("getAssetNull");
return;
}
}
單元測試如下:
package com.hly.unitest.controller;
import com.alibaba.fastjson.JSON;
import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import com.hly.unitest.util.CommonUtil;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import org.springframework.test.util.ReflectionTestUtils;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class, CommonUtil.class})
public class UserServicePowerRunnerTest {
@InjectMocks
private UserService userService;
@Mock
private UserInfoMapper userInfoMapper;
@Before
public void setUp() {
// mock私有方法
userService = PowerMockito.spy(new UserService());
ReflectionTestUtils.setField(userService, "userInfoMapper", userInfoMapper);
}
@Test
public void getUserInfo() throws Exception {
UserInfo userInfo = mockUserInfo();
when(userInfoMapper.selectByUserId(any())).thenReturn(userInfo);
// 控制私有方法的返回值
PowerMockito.doReturn("un").when(userService, "getAssetStr0", any(), any());
UserInfo result = userService.getUserInfo("6f49aa9e-1afc-4439-ad21-25ae90dde566");
System.out.println(JSON.toJSONString(result));
}
@Test
public void testStaticMethod() throws Exception {
String result = userService.testStaticMethod();
System.out.println("the 1st time: result = " + result);
// mock靜態方法
PowerMockito.mockStatic(CommonUtil.class);
String str = "123";
when(CommonUtil.generateUUID()).thenReturn(str);
result = userService.testStaticMethod();
System.out.println("the 2nd time: result = " + result);
}
@Test
public void testClassVariables() throws Exception {
String result = userService.testClassVariables();
System.out.println("the 1st time: result = " + result);
// mock類變量
Whitebox.setInternalState(userService, "finalStr", "FINAL-STR");
result = userService.testClassVariables();
System.out.println("the 2nd time: result = " + result);
}
private UserInfo mockUserInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setUserId("654321");
userInfo.setUserName("B");
userInfo.setPassword("pwB");
userInfo.setGender("女");
return userInfo;
}
}
測試getUserInfo返回:
測試testStaticMethod返回:
測試testClassVariables返回:
分析:
可以看到各個測試用例的返回都符合預期,而且沒有啓動Spring,這極大的縮短了執行時間,方便了測試用例的編寫,所以目前來看,我推薦大家使用PowerMockRunner,輕量級的使用,測試覆蓋分支也是極方便的。
提出問題: 我們項目組的覆蓋率插件使用的是jacoco,但是PowerMockRunner和jacoco不兼容,使得我們項目的覆蓋率一直在10%左右徘徊,對於怎麼解決這個問題,一直以來,頭特別的大,在查了很多資料之後找到一個解決方案。