0.写在之前
文章写于2023年12月12日,前年大概也是这时期接到需求,所有项目的代码覆盖率至少要达到30%,当即开始从网上搜集资料学习单元测试。
距离当时的代码已过两年有余,想来也是十分惭愧。至于原因,一方面可能是身体中的堕虫在作祟,毕竟距离上次在掘金写作已经是一年前的事情了;另一方面就是技术上确实没有什么大的提升,能写的网上都能搜到,再抄一遍出来也没有什么意义。但这都不是理由,经常记录该是个好习惯的。
这次是领导要求一周稍微加几天班,但是又没事做,正好趁此机会整理一下,方便以后遇到此类需求不用再去翻代码。
接下来本文将从实操出发,记录如何使用mockito在springboot项目中进行单元测试。
1.开始使用
1.1 整理思路
- 首先明确目标,我们是要通过单元测试提升代码的覆盖率,那在对每个方法进行测试时,不可避免就有以下几个步骤:
1、构造方法的入参 以及想要让它返回的出参,构建不同的参数即可覆盖代码的不同分支。
2、进行打桩。打桩:在被测试的方法中,如果调用了其他类中的方法,可以在测试方法中对其进行打桩,让单元测试执行到此处时返回自己想要的结果,具体请参见下文。
3、执行被测试类的该方法获取结果,然后比较结果与预期是否相符。
1.2 引入依赖
- 使用的
springboot
版本为2.3.2.RELEASE
,spring-boot-starter-test
依赖包中包含有mockito
和junit
,但在mock静态方法时可能会出现版本问题,所以此处给出详细版本供参考。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.8.0</version>
<scope>test</scope>
</dependency>
2.例
2.1 准备工作
-
1、找到需要测试的类,如
AppParamsServiceImpl
,在与main
同级的test
目录下建立与该类包结构相同的测试类,可命名为AppParamsServiceImplTest
。 -
2、为测试类添加注解
@RunWith(MockitoJUnitRunner.class)
,并将被测试的类作为测试类的属性,为其添加注解@InjectMocks
,将被测试类中要使用的接口也添加进来,并添加注解@Mock
。 -
3、添加测试方法,为其添加注解
@Test
,一般使用test开头作为方法名,如testGetAppParams()
,无需参数。
2.2 代码
- 以下为一个被测试类 和测试类 的例子,按1.1步骤编写,但一些测试类中并不需要包含全部步骤。已省略与单元测试无关的导入,请配合注释阅读。
2.2.1 被测试类
java
@Service
@Slf4j
public class AppParamsServiceImpl extends ServiceImpl<AppParamsMapper, AppParams> implements AppParamsService {
@Resource
AppParamsMapper appParamsMapper;
@Override
public AppResponse getAppParams(String id) {
QueryWrapper<AppParams> wrapper = new QueryWrapper<>();
wrapper.eq("app_params_id", id);
wrapper.ne("state", "X");
AppParams appParams = appParamsMapper.selectOne(wrapper);
AppResponse appResponse = new AppResponse();
BeanUtils.copyProperties(appParams, appResponse);
return appResponse;
}
}
2.2.2 测试类
java
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class AppParamsServiceImplTest {
@InjectMocks
private AppParamsServiceImpl appParamsServiceImplUnderTest;
@Mock
private AppParamsMapper mockAppParamsMapper;
@Test
public void testGetAppParams() {
// 定义期望的返回结果
final AppResponse expectedResult = new AppResponse();
expectedResult.setUrl("url");
expectedResult.setApi("api");
expectedResult.setAppId("appId");
expectedResult.setAppKey("appKey");
// 该执行:AppParams appParams = appParamsMapper.selectOne(wrapper);
// 不想连接数据库进行查询,但想让单元测试继续进行,则对此处进行打桩
// 为打桩做准备,构建想要其返回的结果
final AppParams appParams = new AppParams();
appParams.setAppParamsId("appParamsId");
appParams.setUrl("url");
appParams.setApi("api");
appParams.setAppId("appId");
appParams.setAppKey("appKey");
// 打桩 当执行到selectOne方法,且参数为任意的QueryWrapper,则返回之前定义好的appParams
when(mockAppParamsMapper.selectOne(any(QueryWrapper.class))).thenReturn(appParams);
// 调用被测试类的方法进行测试
final AppResponse result = appParamsServiceImplUnderTest.getAppParams("id");
// 校验结果
assertEquals(expectedResult, result);
}
}
2.3 注意
2.3.1 常量
- 如果被测试类中包含常量或者从配置文件中获取的变量,可使用以下方法进行mock:
java
import org.springframework.test.util.ReflectionTestUtils;
import org.junit.jupiter.api.BeforeEach;
import static org.mockito.MockitoAnnotations.initMocks;
/**
* 在测试类中添加该方法,在每个测试方法中先调用setUp()对变量进行设置,或添加 @BeforeEach注解
*/
@BeforeEach
void setUp() {
initMocks(this);
ReflectionTestUtils.setField(appParamsServiceImplUnderTest,"channelId","channelId");
}
2.3.2 异常
- 测试抛出异常的分支时可以使用
assertThrows
:
java
import static org.testng.Assert.assertThrows;
@Test
public void testAssertUtil() {
assertThrows(ValidateException.class, () -> AssertUtil.isNotNull(null, ValidateMessageCodeEnum.CHANNEL_ID_IS_NULL));
}
2.3.3 静态方法
- 如果被测试类的方法中调用了静态类的静态方法,则与上方稍有不同,可使用以下方法进行mock:
java
import com.owner.mos.mops.infiniti.service.common.utils.HttpClientUtil;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@RunWith(MockitoJUnitRunner.class)
public class OwnerServiceTest {
@InjectMocks
private OwnerService ownerServiceUnderTest;
@MockBean
private MockedStatic<HttpClientUtil> httpClientUtilMockedStatic;
@BeforeEach
void setUp() {
initMocks(this);
// 对静态类进行mock
httpClientUtilMockedStatic = mockStatic(HttpClientUtil.class);
}
@Test
public void testNotify() {
setUp();
// 参数构造...
// 期望结果
final HttpRespVO expectedResult = new HttpRespVO("成功", new String[]{"操作成功"}, null);
// mock方法返回值
HttpRespDTO httpRespDTO = new HttpRespDTO();
httpRespDTO.setMsg("操作成功");
String resultJson = JSON.toJSONString(httpRespDTO);
// 打桩
httpClientUtilMockedStatic.when(() -> HttpClientUtil.doPostMethodNoVerify(anyString(), anyString(), anyMap())).thenReturn(resultJson);
// 调用测试
final HttpRespVO result = ownerServiceUnderTest.notify(notifyMessage);
// 校验结果
assertEquals(expectedResult, result);
// 最后将其及时关闭
httpClientUtilMockedStatic.close();
}
}
2.3.4 打桩参数
- 没有参数的可以不填,
String
类型可设为anyString()
,其他的包装类型也类似。如:
java
when(mockOrderSequence.nextValue()).thenReturn("result");
httpClientUtilMockedStatic.when(() -> HttpClientUtil.doPostMethodNoVerify(anyString(), anyString(), anyMap())).thenReturn(resultJson);
- 有参数的一般可直接设为
any()
,有时也可使用其他值,请按实际情况使用,如:
java
when(mockOrderService.addOrder(new OrderAddDto())).thenReturn(orderAddVo);
when(mockAppParamsMapper.selectOne(any(QueryWrapper.class))).thenReturn(appParams);
when(mockOrderMapper.update(eq(new Order()), any(LambdaQueryWrapper.class))).thenReturn(0);
when(mockRestTemplate.postForObject(anyString(), any(), eq(WsResponse.class), any(Object.class))).thenReturn(wsResponse);
3.插件
Squaretest
,idea中一款单元测试插件,可按模板生成单元测试文件,节省一部分工作量,但较多一部分仍需按实际情况自行编写,且为免费试用30天。使用方法请读者自行搜索。
4.写在之后
本文介绍了进行单元测试的一般流程与使用时的注意项,是作者解决刚接触时遇到的问题而形成的方法论,作为自身技术成长的记录,也希望能为需要的人提供一些帮助。
本文若有学术错误或形容不恰当的地方,还请读者们多多指教!
最后还是感谢掘金为大家提供了一个相对较好的平台,让我能安下心来写下这第二篇文章。