# 单元测试小技巧
# 前言
# 工具
- junit5
- 实用Mockio进行mock
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
目的:单测覆盖率100%!!!!
# 插件推荐
# Squaretest
写单测算是个比较无聊且冗长的事情,但这是规范!(手动狗头!),要是有个插件能帮我们处理一些事就是极好的。
这里推荐一下Squaretest(免费30天),可以帮我们把模板代码生成,对于不会写单测或者说对写单测不太熟悉的同学,就是一个很不错的切入点。
# 实用小技巧
# 1. 查看局部代码的单测覆盖率
- 右键选中要查看覆盖率的子包
- 选择More Run/Debug
- 选中Run Test in "你选中的子包" With Coverage
# 代码测试及参数mock
# 1.有返回参数的方法怎么mock
一般来说,单元测试的目的是校验自己项目本身的代码逻辑,并不会要求跨服务校验。所以这种情况,我们只需要对第三方服务进行mock,用我们期望的返回值去校验本服务的逻辑就好。
# 待测试代码
package com.cn.liuxiaolxx.test;
@Service
public class TestServiceImpl implements TestService {
@DubboReference(version = "1.0.0")
private TestRemoteService testRemoteService;
@Override
public BigDecimal queryList(DescribeReqeust request) {
return testRemoteService.query(request);
}
}
# 单测代码书写
package com.cn.liuxiaolxx.test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {
/**
* 使用注解@Mock对TestRemoteService服务进行mock
*/
@Mock
private TestRemoteService testRemoteService;
/**
* 使用注解@InjectMocks将该service之前声明的被@Mock标记的service注入到本服务中
*/
@InjectMocks
private TestServiceImpl testService;
@Test
public void testQueryList_success_return_result() {
DescribeReqeust request = new DescribeReqeust();
// 当调用testRemoteService.query方法时,无视参数,直接返回10
when(testRemoteService.query(eq(request))).thenReturn(BigDecimal.TEN);
// 调用
BigDecimal actualResult = testService.queryList(request);
// 结果验证
assertThat(actualResult).isEqualTo(BigDecimal.TEN);
// 调用次数验证,默认校验调用1次
verify(testRemoteService).query(eq(request));
}
}
# 2.无返回参数的方法怎么mock
# 待测试代码
package com.cn.liuxiaolxx.test;
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Autowired
private TestCalculateService testCalculateService;
@Override
public void calculate() {
List<BigDecimal> emptyList = Lists.newArrayList();
testCalculateService.calculate(emptyList);
if (CollectionUtils.isNotEmpty(emptyList)) {
testMapper.insertOrUpdate(emptyList);
}
}
}
# 单测代码书写
package com.cn.liuxiaolxx.test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {
@Mock
private TestMapper testMapper;
@Mock
private TestCalculateService testCalculateService;
/**
* 使用注解@InjectMocks将该service之前声明的被@Mock标记的service注入到本服务中
*/
@InjectMocks
private TestServiceImpl testService;
@Test
public void testCalculate() {
Mockito.doAnswer((Answer<Object>)invocation -> {
// 获取第0个参数,若有多个参数的情况,按下标获取即可
List<BigDecimal> priceList = invocation.getArgument(0);
// 在参数中加入1和10
priceList.add(BigDecimal.ONE);
priceList.add(BigDecimal.TEN);
return invocation;
// 当调用testCalculateService的calculate的时候,无视参数
}).when(testCalculateService).calculate(any());
// 当调用testMapper.insertOrUpdate方法时,直接返回成功(影响条数1)
when(testMapper.insertOrUpdate(any())).thenReturn(1);
// 方法调用
testService.calculate();
// 调用次数验证,默认校验调用1次
verify(testCalculateService).calculate(any());
// 调用次数验证,由于list中插入了2条数据,需要验证调用2次
verify(testMapper, times(2)).insertOrUpdate(any());
}
}
# 3.分页处理的任务怎么mock
# 待测试代码
package com.cn.liuxiaolxx.test;
import javax.management.Query;
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Autowired
private Test2Service test2Service;
@Override
public void calculate(String parameter) {
Query query = new Query();
// query set other params
query.setParameter(parameter);
int page = 1;
int size = 1000;
while (true) {
query.setCurrent(page);
query.setSize(size);
List<Result> resultList = testMapper.testQuery(query);
if (CollectionUtils.isEmpty(resultList)) {
break;
}
test2Service.doSomething(resultList);
page++;
}
}
}
# 单测代码书写
这时候我们观察自己的待检测代码,如果还按照之前的方式进行mock,那循环就结束不了了。所以期望在指定参数的情况下,才返回结果。其他时候走break逻辑
package com.cn.liuxiaolxx.test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {
@Mock
private TestMapper testMapper;
@Mock
private Test2Service test2Service;
/**
* 使用注解@InjectMocks将该service之前声明的被@Mock标记的service注入到本服务中
*/
@InjectMocks
private TestServiceImpl testService;
@Test
public void testCalculate() {
// 入参
String parameter = "parameter";
Result result = new Result();
List<Result> resultList = Arrays.asList(result);
// 当参数query的current是1的情况下,才返回指定结果
when(testMapper.testQuery(Mockito.argThat(
query -> query.getCurrent() == 1))).thenReturn(resultList);
// 方法调用
testService.calculate(parameter);
// 调用次数验证,默认校验调用1次
verify(test2Service).doSomething(eq(resultList));
// 调用次数验证, 第一次查询成功,进入逻辑处理,第2次查询为空,跳出循环,需要验证调用2次
verify(testMapper, times(2)).testQuery(any());
}
}
# 4.校验指定异常
# 待测试代码
package com.cn.liuxiaolxx.test;
@Service
public class TestServiceImpl implements TestService {
@Override
public void calculate(String parameter) {
if (Strings.isNullOrEmpty(parameter)) {
throw new RuntimeException("参数异常, parameter不能为空");
}
// doSomething
}
}
# 单测代码书写
这时候我们观察自己的待检测代码,如果还按照之前的方式进行mock,那循环就结束不了了。所以期望在指定参数的情况下,才返回结果。其他时候走break逻辑
package com.cn.liuxiaolxx.test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {
@Mock
private TestServiceImpl testService;
@Test
public void testCalculate() {
// 入参
String parameter = null;
// 方法调用
try {
testService.calculate(parameter);
Assertions.fail("参数异常了,检测后必须抛异常");
} catch (RuntimeException e) {
assertThat(e.getMessage()).isEqualTo("参数异常, parameter不能为空");
}
}
}
# 5. 参数化校验
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* The type IntelliJ IDEA.
* <p>
*
* @author liuxiaolu
* @since 2022/7/8
*/
@ExtendWith(MockitoExtension.class)
class Test {
@ParameterizedTest
@CsvSource(value = {",,0", "-1,-1,-2", "1,,1", ",1,1"})
void test(Integer a, Integer b, Integer result) {
assertThat(result).isEqualTo(add(a, b)).withFailMessage("计算失败,期望结果是" + result);
}
Integer add(Integer a, Integer b) {
if (a == null && b == null) {
return 0;
}
if (a == null) {
return b;
}
if (b == null) {
return a;
}
return a + b;
}
}
# 6. mock重置
在我们写单测的过程中,一般会在一个类中写很多个case,对于每个case我们可能需要mock不同的方法,不同的返回值。在每次mock之后,需要将mock重置,否则可能出现存根异常。
@ExtendWith(MockitoExtension.class)
class AllowanceCalculateServiceTest {
@Mock
private TestMock1 testMock1;
@Mock
private TestMock2 testMock2;
@InjectMocks
private TestService testService;
/**
* 在每个case调用完成之后,重置mock
*/
@AfterEach
void reset() {
Mockito.reset(testMock1, testMock2);
}
// other method
}
# 999.拓展点
# 1. countDownLatch.await()怎么抛InterruptedException
# 待测试代码
package com.cn.liuxiaolxx.test;
@Slf4j
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestCalculateService testCalculateService;
@Override
public void calculate() {
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
testCalculateService.doSomeThing();
} catch (Exception e) {
log.error("逻辑处理异常", Throwables.getStackTraceAsString(e));
} finally {
countDownLatch.countDown();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
log.error("主线程中断,", e);
}
}
}
# 测试示例
通过源码分析我们可以知道,countDownLatch.await()在主线程被中断的时候会抛出异常InterruptedException, 那我们怎么测试抛出InterruptedException异常呢?
package com.cn.liuxiaolxx.test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {
@Mock
private TestCalculateService testCalculateService;
@Test
public void testCalculate_excepted_check_interrupted_exception() {
// 获取主线程
Thread mainThread = Thread.currentThread();
// 当调用testCalculateService的calculate的时候,调用代码块中的逻辑
Mockito.doAnswer((Answer<Object>)invocation -> {
// 在子线程中中断主线程
mainThread.interrupt();
return invocation;
}).when(testCalculateService).doSomeThing();
// 方法调用
testService.calculate();
// 调用次数验证,默认校验调用1次
verify(testCalculateService).doSomeThing(eq(resultList));
}
}