1.什么是 Mock 测试
Mock 通常是指,在测试一个对象 A 时,我们构造一些假的对象(一般是不容易构造或者不容易获取的对象,比如测试类所依赖的实现类、第三方接口、数据库操作对象)来模拟与 A 之间的交互,这些对象被称为 Mock 对象,而 Mock 对象的行为是我们事先设定且符合预期。通过这些 Mock 对象来测试 A 在正常逻辑,异常逻辑或压力情况下工作是否正常。
下面是一个例子:
当我们需要测试 OrderService 时,按照我们常规的做法呢,都是要先准备好 redis,跟 db 的环境,然后构造UserService 跟 CouponService 注入进来,此时需要构建完整的依赖树,其过程是比较繁琐的,万一数据库连不上,依赖找不到,服务挂了... 时间一长可能会打击我们对项目进行单测的积极性,所以这时候很有必要寻求一种优雅的方式来解决。即,把这些依赖都转成 Mock 对象,作为模拟,将测试的重点专注于开发的功能逻辑。
2.常用注解
2.1@Mock
创建一个模拟对象,并注入到测试类中。通常和 @InjectMocks 注解一起使用,用于为测试类中的被测对象注入模拟对象。当你使用 @Mock 注解一个对象时,Mockito 会为你创建一个该对象的模拟实例。这个模拟实例的行为(即方法的返回值和它们被调用的方式)可以被精确地控制和验证。默认情况下,模拟对象上的所有方法调用都会返回 null(对于引用类型)或默认值(对于原始类型)。
可以通过插桩的方式来模拟对象的行为。
2.2@Spy
用于创建一个部分模拟对象,它也属于 mock 对象的一种。但与@Mock不同,@Spy创建的是一个实际对象的代理,并且允许你选择性地模拟对象中的某些方法。默认情况下,所有未被特别模拟的方法都会调用实际对象的方法,通常与 doReturn/doThrow 等方法结合使用。一般是加在要测试的方法所在的类对象上,即加了 @InjectMocks 注解的位置。
2.3@InjectMocks
一般标注在被测对象上(必须是实现类),因为 mockito 会为添加了 @InjectMocks 注解的属性创建对应的实例对象。默认创建的对象就是一个普通的对象。经常需要配合 @Spy 注解使其变为默认调用真实方法的 mock 对象(可以理解为一个支持插桩的真实对象)。此外,mockito 会将 @Mock 和 @Spy 标注的对象注入到添加了 @InjectMocks 注解的被测对象中。
java
@InjectMocks
@Spy
private UserServiceImpl UserServiceImplTest;
@Mock
private UserFeatureService userFeatureService;
@Mock
private List<String> mockList;
一点总结:
方法插桩 | 方法不插桩 | 作用对象 | |
---|---|---|---|
Mock 对象 | 执行插桩逻辑 | 返回 mock 对象的默认值(不会去调用真实实现类方法) | 被测试类的依赖项 |
Spy 对象 | 执行插桩逻辑 | 调用真实方法 | 被测试的实现类 |
3.常见操作
3.1插桩
- when(xxxService.someMethod()).thenXxx(...):其中 xxxService 可以是 mock 对象。
- doXXX().when(xxxService).someMethod():其中 xxxService 可以是 mock/spy 对象。
推荐是使用 doXXX()... 的插桩方式,因为对于 spy 对象,如果采用 when(...)... 的方式,会先执行真实的方法,比如会先执行一段打印的逻辑,而这不是我们想要的(即使由于插桩,最终返回的是插桩时指定的返回)。
常见的几种用法:
- 指定返回
- 指定抛出异常
- 多次插桩
- thenAnswer 指定实现自定义逻辑的插桩
java
@Test
public void test()(
when(mockList.get(anyInt())).thenAnswer(newAnswer<String>(){
/**
*泛型表示要插桩方法的返回值类型
*/
@override
public String answer(InvocationOnMock invocation) throws Throwable{
// getArgument表示获取插桩方法(此处就是List.get)的第几个参数值
Integer argument = invocation.getArgument(index: 0, Integer.class);
return String.valueof(argument* 100);
}
});
String result = mockList.get(3);
Assertions.assertEquals(expected:"3ee",result);
}
3.2执行真实的方法
Mock 对象也可以执行真实的方法,只要在插桩时指定让他去调用真实方法即可。
Spy 对象,不插桩时即是执行真实方法。
3.3verify
可以用于验证某个方法是否被调用、被调用了几次、至少/至多调用了几次。且可以指定匹配的参数。此外,即使该方法被调用多次,但参数未匹配上,那也是不算的。
Verify 的另一个作用是对没有返回值的方法进行验证,证明其至少是被调用过的。
java
@Mock
private UserService userService;
@Test
public void test1() {
userService.addUser("张三", "25", "男")
// 通过
verify(userService, times(1)).addUser("张三","25","男");
// 不通过
verify(userService, times(1)).addUser("李四","25","男");
// 通过
verify(userService, times(1)).addUser(anyString(),anyString(),anyString());
}
3.4断言
断言可用于对有返回值的方法进行判断。
也可以对没有返回值的方法进行判断:
java
Assertions.assertDoesNotThrow(() -> userServiceImplUnderTest.add("张三","25"));
4.基本思路
- 对于非测试方法进行 mock(一般是插桩),一般是数据库,或其他类方法的调用;
- 调用需要测试的方法;
- 实时修改(插桩、添加断言等);
5.参考
- 视频,有案例,讲的挺清楚的。mockito加junit搞定单元测试
- Java单元测试神器之Mockito - 掘金
- 这篇很全:Mockito测试框架入门与使用
- 对mock基本概念的介绍挺好的:单元测试 - Mockito 详解
- 单元测试 - Mockito 详解