Mockito单测之道
去年写过一篇《TestNG单元测试实战》文章,严格来讲算集成测试。
没看的小伙伴可直接看本篇即可,本质是单元测试框架不同,写法不一样。
单测定义
单元测试定义:
对软件中最小可测单元进行验证,可理解为对一个类中公有、私有方法的测试验证。
单元测试原则:
1. 快速的。
不依赖外部环境比如数据库mysql,不依赖springboot应用启动,本质是执行一个函数,一个方法,所以理应是能快速运行的。
2. 自动的。
单元测试应该是全自动执行的,非交互式的。
3. 独立的。
单元测试用例之间是独立运行的,用例互相之间无依赖,对外部资源也无依赖。
4. 可重复的。
单元测试是可重复执行的,在被测代码不变的情况下,每次执行结果是一致的。
单元测试用例主要有以下特点:
1. 不依赖外部环境和数据;
2. 不需要启动应用和初始化对象;
3. 需要使用@Mock来初始化依赖对象,用@InjectMocks来初始化测试对象;
4. 需要自己模拟依赖方法,指定什么参数返回什么值或异常;
5. 返回值确定,可用Assert相关方法进行断言;
6. 可以验证依赖方法的调用次数和参数值,还可以验证依赖对象的方法是否调用完毕。
什么是有效的单元测试
-
验证依赖方法,验证调用次数;
-
使用明确语义的断言;
-
验证异常抛出;
-
验证数据对象属性;
-
用mock对象代替真实对象的注入;
......
单元测试和集成测试区别:
单元测试是对软件设计中最小单元的程序模块进行测试,集成测试是对这些程序模块组装成的系统模块进行测试。(单元测试没有外部依赖,集成测试也可能没有外部依赖)
集成测试用例主要有以下特点:
1. 依赖外部环境和数据;
2. 需要启动应用并初始化测试对象;
3. 使用自动注入机制注入测试对象;
4. 返回值具有不确定性,无法验证。
综上所述,为了更好的验证软件质量,更容易的去测试业务逻辑,推荐编写单元测试来完成代码的用例编写。
实战
环境依赖引入
springboot版本:2.2.2.RELEASE
引入Maven:
<!-- springboot项目加入junit依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test 依赖中包含了junit 和 mockito框架。
本文用Junit5
+ Mockito
来编写单测。
Junit5 = Junit Platform + Junit Jupiter + Junit Vintage
IDEA工具推荐下载 TestMe
插件,自动生成用例类,生成通用模板后再进行修改。
项目案例编写
从一个简单的例子入手
被测 service 对象:
@Service
public class UserServiceImpl {
@Autowired
private IOrderService orderService;
private static List<Long> ids;
@Override
public int activeNumber() {
LambdaQueryWrapper wrapper = Wrappers.<Order>lambdaQuery()
.in(Order::getId, ids);
int count = orderService.count(wrapper);
return count;
}
}
单元测试用例类:
public class UserServiceImplTest {
/**
* 定义测试对象
*/
@InjectMocks
UserServiceImpl userServiceImpl;
/**
* 模拟依赖对象
*/
@Mock
IOrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
void testActiveNumber() {
Mockito.doReturn(1).when(orderService).count(Mockito.any());
int result = userServiceImpl.activeNumber();
Assertions.assertEquals(1, result, "数据结果不一致");
Mockito.verify(orderService).count(Mockito.any());
}
}
使Mockito的注解生效:
Junit4中:
类上添加 @RunWith(MockitoJUnitRunner.class)。
@Before注解方法加 MockitoAnnotations.openMocks(this)。
Junit5中:
类上添加 @ExtendWith(MockitoExtension.class)。
@BeforeEach注解方法加 MockitoAnnotations.initMocks(this)。(版本不同,写法不一样)
一个有依赖的单元测试实施步骤:
一. 定义被测对象。
用@Mock、@Spy、@InjectMocks 相关注解定义测试对象。
-
@Mock:该注解创建一个Mock对象,调用其方法时
不会走真实方法
。 -
@Spy:该注解创建了一个没有Mock的对象,调用其方法是
会走真实方法
。 -
@InjectMocks:创建一个实例,调用代码会走真实方法。会自动注入@Spy和@Mock标注的对象。
/**
* 定义测试对象
*/
@InjectMocks
UserServiceImpl userServiceImpl;
/**
* 模拟依赖对象
*/
@Mock
IOrderService orderService;
如果有静态类成员字段需要注入: 可用ReflectonTestUtils工具注入属性。
ReflectionTestUtils.setField(userServiceImpl, "ids", ids);
二. 模拟依赖对象
模拟依赖对象方法返回,包含参数的返回、异常的返回等。
//模拟orderService.count() 方法调用,调用时返回值为1。
Mockito.doReturn(1).when(orderService).count(Mockito.any());
Mockito.doReturn().when() 和 Mockito.when().thenReturn() 都用于模拟对象方法,在Mock实例下都可使用。
但在 Spy实例下使用时, when().thenReturn() 模式会执行原方法,而 doReturn().when()模式不会执行原方法。
Mock实例下
Mockito.doReturn().when()
/Mockito.when().thenReturn()
都会走模拟方法。Spy实例下
Mockito.doReturn().when()
走模拟调用Mockito.when().thenReturn()
走真实调用
推荐使用doReturn/when。如果不关心具体的参数内容,可用Mockito.any() 代替。
三. 调用被测对象
//模拟依赖方法
int result = userServiceImpl.activeNumber();
//调用被测对象
Assertions.assertEquals(1, result, "数据结果不一致");
Mockito.verify(orderService).count(Mockito.any());
代码中的例子比较简单,假如要模拟的参数是一个对象,则可用JSONObject来验证,提前创建json字符串在文件中,最终转成字符串比较即可。
//例子
String text = ResourceHelper.getResourceAsString(getClass(), "text.json");
List<T> list = JSON.parseArray(text, T.class);
Mockito.doReturn(list).when(service).list(Mockito.any());
//调用依赖方法
List<Map<String, String>> result = serviceA.method();
String text_expected = ResourceHelper.getResourceAsString(getClass(), "text_expected.json");
//映射排序字段,保证key、value字段有序性
Assertions.assertEquals(text_expected, JSON.toJSONString(result, SerializerFeature.MapSortField));
Mockito.verify(service).list(Mockito.any());
//getResourceAsString() 来自参考资料。
public static <T> String getResourceAsString(Class<T> clazz, String name) {
try (InputStream is = clazz.getClassLoader().getResourceAsStream(name)) {
return IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalArgumentException(String.format("以字符串方式获取资源(%s)异常", name), e);
}
}
四. 验证方法调用
确认被测方法是否按预期方法进行了调用。
<span style="color:red"><span style="color:red"><span style="color:red"><code>//验证依赖方法,默认验证调用一次
Mockito.verify(orderService).count(Mockito.any());
</code></span></span></span>
验证多次调用可通过 Mockito.times()调整,Mockito还支持 最少一次调用
、最多一次调用
等方法验证。
验证调用3次
Mockito.verify(service, Mockito.times(3)).count(Mockito.any());
也可通过 verifyNoMoreInteractions()
验证依赖对象,以确保所有调用都得到验证。
单测技巧
1. 使用JSON序列化简化预期值的比较。
在test/resources 目录下,定义预期值的 json文件。减少验证对象代码的编写。
//和 getResourceAsString() 方法类似,读取json文件数据。
try {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(<span style="color:#50a14f">"testActiveNumberWithResult.txt"</span>);
String text = IOUtils.toString(is, <span style="color:#50a14f">"UTF-8"</span>);
//JSON转成对应的list、map即可
List list = JSON.parseObject(text, List.class);
} catch (IOException e) {
//捕获异常
throw new RuntimeException(e);
}
2、通过Assert.assertThrows 来验证方法异常抛出。
支持验证自定义属性、支持验证依赖方法及其参数。
Assertions.assertThrows(RuntimeException.class, () -> Integer.parseInt("1"), "未返回期望异常");
3. ArgumentCaptor 类捕获参数值。
Mockito 提供 ArgumentCaptor
类来捕获参数值,通过调用 forClass(Class<T> clazz)
方法来构建一个 ArgumentCaptor 对象,然后在验证方法调用时来捕获参数,最后获取到捕获的参数值并验证。
如果一个方法有多个参数都要捕获并验证,那就需要创建多个 ArgumentCaptor 对象。
ArgumentCaptor 的主要接口方法:
-
capture():用于捕获方法参数;
-
getValue():用于获取捕获的参数值,如果捕获了多个参数值,该方法只返回最后一个参数值;
-
getAllValues():用户获取捕获的所有参数值。
//代码实例
ArgumentCaptor<Wrapper> captor = ArgumentCaptor.forClass(Wrapper.class);
Mockito.verify(service).list(captor.capture());
Wrapper wrapper = captor.getValue();
参考资料
-
https://developer.aliyun.com/ebook/7895