一、Mock 类框架的使用场景
在实际软件开发中,要进行测试的方法存在外部依赖(如 db,redis,第三方接口调用等),这些外部依赖可能存在各种问题,例如不稳定、缺乏数据、难以模拟等等,所以为了能够专注于对该方法(单元)的 逻辑 进行测试,就希望能虚拟出外部依赖,避免外部依赖成为测试的阻塞项,一般都是测试 service 层即可。
使用 Mock 就可以解决这些问题,Mock 框架他本是是不依赖于其他任何外部依赖的,使用 Mock 对象可以模拟外部依赖的行为和状态,所以相当于将测试方法和外部依赖隔离,能够更好的对单元方法的逻辑测试。
二、Mockito
1、Mockito 简介
Java 中主流的 Mock 框架
Mockito 支持情况:
限制: 老版本对于 final class、final method、staticmethod、private method 均不能被 mockitomock,目前已支持 final class、final method、staticmethod 的 mock
官网文档: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
spring-boot-starter-test
已经集成了mockito
,所以 SpringBoot 项目无需另外引入依赖。
mock 对象与 spy 对象
Mock 对象是一个完全模拟的对象,用于单元测试中替代真实对象。它允许开发者定义方法的预期行为,而不执行任何实际的业务逻辑。这种对象常用于隔离被测类的依赖,以确保测试的独立性和纯粹性。Mock 对象在方法调用时会返回预设的结果,帮助测试者验证方法调用的正确性和交互行为。
Spy 对象是一个部分模拟的对象,用于单元测试中模拟部分行为,同时保留对象的其他真实行为。Spy 对象在未定义行为的方法调用时,会执行真实对象的方法。它允许开发者在某些方法上插桩,而在其他方法上执行真实逻辑。
作用对象 | 方法插桩 | 方法不插桩 | 备注 |
---|---|---|---|
mock 对象 | 执行插桩逻辑 | 返回 mock 对象的默认值 | 接口或实现类,用于创建完全虚拟的 mock 对象,无需实际实现 |
spy 对象 | 执行插桩逻辑 | 调用真实方法 | 类,用于创建部分 mock、部分真实的 spy 对象,需要实际实例 |
插桩: 指定调用某个方法时的行为,一般是指调用某个方法时的返回值。
不插桩: 使用模拟对象的方法默认行为(mock 对象返回默认值,spy 对象调用真实方法)
2、初始化 mock 和 spy 对象的三种方式
- junit4:@RunWith(MockitoJUnitRunner.class)+@Mock 等注解
- junit5:@Extendwith(MockitoExtension.class)+@Mock 等注解
@ExtendWith(MockitoExtension.class)
public class InitMockito {
@Mock
private IUserService mockUserService;
@Spy
private IUserService spyUserService;
@Test
public void testMock1() {
// 判断某对象是不是 mock 对象 -- true
Assertions.assertTrue(Mockito.mockingDetails(mockUserService).isMock());
// 判断某对象是不是 spy 对象 -- false
Assertions.assertFalse(Mockito.mockingDetails(mockUserService).isSpy());
// 判断某对象是不是 mock 对象 -- true -- spy 对象是另一种不同类型的 mock 对象
Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isMock());
// 判断某对象是不是 spy 对象 -- true
Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isSpy());
}
}
- Mockito.mock(X.class) 等静态方法
public class InitMockito2 {
private IUserService mockUserService;
private IUserService spyUserService;
@BeforeEach
public void init() {
mockUserService = Mockito.mock(IUserService.class);
spyUserService = Mockito.spy(IUserService.class);
}
@Test
public void testMock1() {
// 判断某对象是不是 mock 对象 -- true
Assertions.assertTrue(Mockito.mockingDetails(mockUserService).isMock());
// 判断某对象是不是 spy 对象 -- false
Assertions.assertFalse(Mockito.mockingDetails(mockUserService).isSpy());
// 判断某对象是不是 mock 对象 -- true -- spy 对象是另一种不同类型的 mock 对象
Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isMock());
// 判断某对象是不是 spy 对象 -- true
Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isSpy());
}
}
- MockitoAnnotations.openMocks(this)+@Mock 等注解
public class InitMockito3 {
@Mock
private IUserService mockUserService;
@Spy
private IUserService spyUserService;
@BeforeEach
public void init() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testMock1() {
// 判断某对象是不是 mock 对象 -- true
Assertions.assertTrue(Mockito.mockingDetails(mockUserService).isMock());
// 判断某对象是不是 spy 对象 -- false
Assertions.assertFalse(Mockito.mockingDetails(mockUserService).isSpy());
// 判断某对象是不是 mock 对象 -- true -- spy 对象是另一种不同类型的 mock 对象
Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isMock());
// 判断某对象是不是 spy 对象 -- true
Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isSpy());
}
}
3、参数匹配
在 Mockito 中,参数匹配器(ArgumentMatchers)用于指定在模拟对象的方法被调用时匹配某些参数的规则。这样可以灵活地定义方法行为,而不必精确匹配所有参数。Mockito 提供了多种参数匹配器,可以匹配各种类型和条件的参数。
测试参数匹配
@ExtendWith(MockitoExtension.class)
public class MockitoMatchParamsTest {
@Mock
private IUserService mockUserService;
@Test
public void test1() {
// 方法插桩
Mockito.when(mockUserService.login("lee","123")).thenReturn(new User());
Mockito.doReturn(new User()).when(mockUserService).login("boy","456");
// 调用方法
mockUserService.login("lee","123");
mockUserService.login("boy","456");
// 验证 mock 方法是否按预期被调用次数
Mockito.verify(mockUserService,Mockito.times(1)).login(ArgumentMatchers.eq("lee"),ArgumentMatchers.anyString());
Mockito.verify(mockUserService,Mockito.times(1)).login(ArgumentMatchers.anyString(),ArgumentMatchers.eq("456"));
Mockito.verify(mockUserService,Mockito.times(2)).login(ArgumentMatchers.anyString(),ArgumentMatchers.anyString());
}
}
注意: Mockito 的参数匹配器需要与非匹配器参数分开使用,不能混合。
// 错误:混合使用匹配器和非匹配器参数
when(myService.performAction(anyString(), "fixed value")).thenReturn("Mocked Result");
正确写法:
// 正确:所有参数都使用匹配器
when(myService.performAction(anyString(), eq("fixed value"))).thenReturn("Mocked Result");
// 正确:所有参数都不使用匹配器
when(myService.performAction("specific input", "fixed value")).thenReturn("Mocked Result");
4、方法插桩
插桩: 指定调用某个方法时的行为,一般是指调用某个方法时的返回值。
不插桩: 使用模拟对象的方法默认行为(mock 对象返回默认值,spy 对象调用真实方法)。
在 Mockito 中,mock 和 spy 对象在不进行方法插桩的情况下有不同的默认行为。下面是对它们默认值行为的详细说明。
下表,展示了在不插桩的情况下,mock 和 spy 对象的默认值情况:
对象类型 | Mock 对象默认值 | Spy 对象默认值 |
---|---|---|
对象类型 | null | 调用真实对象的方法返回值 |
boolean | false | 调用真实对象的方法返回值 |
int | 0 | 调用真实对象的方法返回值 |
short | 0 | 调用真实对象的方法返回值 |
byte | 0 | 调用真实对象的方法返回值 |
long | 0L | 调用真实对象的方法返回值 |
float | 0.0f | 调用真实对象的方法返回值 |
double | 0.0d | 调用真实对象的方法返回值 |
集合类型 | 空集合或数组 | 调用真实对象的方法返回值 |
void 方法 | 什么都不做 | 调用真实对象的方法(有副作用) |
(1)Mockito 提供了两种插桩方式
- when(obj.someMethod().thenXxx()–其中 obj 可以是 mock 对象,不适用无返回值的方法
- doXxx().when(obj).someMethod()–其中 obj 可以是 mock/spy 对象,也适用于没有返回值的方法 注意:spy 对象写在 when 中会先执行一次原方法,即使插桩也起不到 mock 的目的
@ExtendWith(MockitoExtension.class)
public class MockitoStubbingTest {
@Mock
private IUserService mockUserService;
@Spy
private UserServiceImpl spyUserService; // spy 作用于实现类
@Test
public void testStubbing() {
// 1. mock 和 spy 对象都使用 when-then 进行插桩
when(mockUserService.realMethod()).thenReturn("test-mockMethod!");
when(spyUserService.realMethod()).thenReturn("test-spyMethod!");
// 结果:spy 对象写在 when 中会先执行一次原方法,即使插桩也起不到 mock 的目的
System.out.println(mockUserService.realMethod());
System.out.println(spyUserService.realMethod());
}
}
正确写法:
@Test
public void testStubbing2() {
// 1. mock 和 spy 对象都使用 when-then 进行插桩
when(mockUserService.realMethod()).thenReturn("test-mockMethod!");
doReturn("test-spyMethod!").when(spyUserService).realMethod();
// 结果:spy 对象写在 when 中会先执行一次原方法,及时插桩也起不到 mock 的目的
System.out.println(mockUserService.realMethod());
System.out.println(spyUserService.realMethod());
}
(2)void 返回值方法插桩
使用 doNothing().when().yourMethod():
@Test
public void testStubbing3() {
doNothing().when(mockUserService).modefyPwd(anyString(),anyString(),any(User.class));
mockUserService.modefyPwd("123","456",new User());
}
(3)抛异常
两种方式:
- doThrow - when
- when - thenThrow
@Test
public void testThrowException() {
// 方法一:使用 doThrow-when
doThrow(RuntimeException.class).when(mockUserService).realMethod();
try {
mockUserService.realMethod();
// 如果未抛出异常,这 mock 失败
Assertions.fail();
} catch (Exception e) {
// 断言异常类型
Assertions.assertTrue(e instanceof RuntimeException);
}
// 方法二:使用 when-throw
when(mockUserService.login("boy","123")).thenThrow(RuntimeException.class);
try {
mockUserService.login("boy","123");
// 如果未抛出异常,这 mock 失败
Assertions.fail();
} catch (Exception e) {
// 断言异常类型
Assertions.assertTrue(e instanceof RuntimeException);
}
}
(4)多次插桩
@Test
public void multiStubbing() {
// 第一次调用返回 1,第二次调用返回 2,第三次调用返回 3,第四次调用返回 3 ...
// doReturn(1l).doReturn(2l).doReturn(3l).when(mockUserService).count();
// doReturn(1l,2l,3l).when(mockUserService).count();
// when(mockUserService.count()).thenReturn(1l).thenReturn(2l).thenReturn(3l);
when(mockUserService.count()).thenReturn(1l,2l,3l);
Assertions.assertEquals(1,mockUserService.count());
Assertions.assertEquals(2,mockUserService.count());
Assertions.assertEquals(3,mockUserService.count());
Assertions.assertEquals(3,mockUserService.count());
}
(5)thenAnswer 指定插桩逻辑
- 调用 thenAnswer 方法,重写 Answer
- 调用 doAnswer 方法,重写 Answer 使用 thenAnswer 时
注意: 泛型表示[插桩方法的返回值]
@Test
public void thenAnswerStubbing() {
when(mockUserService.login(anyString(),anyString())).thenAnswer(new Answer<User>() {
/**
* 泛型表示[插桩方法的返回值]
*/
@Override
public User answer(InvocationOnMock invocationOnMock) throws Throwable {
// getArgument 表示获取插桩方法的第几个参数值
String name = invocationOnMock.getArgument(0, String.class);
String pasw = invocationOnMock.getArgument(1, String.class);
// 重组数据
User user = new User();
user.setUsername("用户名:"+name);
user.setPassword("密码:"+pasw);
return user;
}
});
// 或者使用 doAnswer()
// 调用方法
User user = mockUserService.login("lee", "123");
// 断言
Assertions.assertEquals("用户名:lee",user.getUsername());
Assertions.assertEquals("密码:123",user.getPassword());
}
(6)执行真正的原始方法
- 调用 thenCallRealMethod 方法
- 调用 doCallRealMethod 方法
注意: 要求此时 @Mock 注解标记的是 实现类。
@Test
public void testMockRealMethod() {
when(mockUserService.realMethod()).thenCallRealMethod();
// doCallRealMethod().when(mockUserService).realMethod();
// 调用
String res = mockUserService.realMethod();
// 断言
Assertions.assertEquals("service-realMethod!",res);
}
(7)verify 的使用
verify 方法主要用于验证 mock 对象的方法是否按预期被调用。也可以验证方法的调用次数、顺序和参数。
@Test
public void testMockVerify() {
// 默认返回 null (没有插桩)
User user = mockUserService.login("lee", "123");
Assertions.assertNull(user);
// 验证调用过 1 次 login 方法,且参数是 "lee","123"
verify(mockUserService).login("lee","123");
// 等价于上面 vetify
verify(mockUserService,times(1)).login("lee","123");
// 校验没有调用的两种方式
verify(mockUserService,times(0)).count();
verify(mockUserService,never()).count();
// 校验最少或最多调用了多少次
verify(mockUserService,atLeast(1)).login("lee","123");
verify(mockUserService,atMost(1)).login("lee","123");
}
5、@InjectMocks 注解使用
@InjectMocks 注解的主要作用是将使用 @Mock 或 @Spy 注解创建的 mock 对象或 spy 对象注入到被测试对象中,以便在测试中使用这些对象。它可以自动识别和匹配需要注入的字段,无论是通过构造函数、字段注入还是方法注入。
注意: 被 @InjectMocks 标注的属性必须是实现类,因为 mockito 会创建对应的实例对象,默认创建的对象就是未经过 mockito 处理的普通对象,因此常配合 @Spy 注解使其变为默认调用真实方法的 mock 对象。一般被测试的类,都需要标注这两个注解。
// 被测试的服务类
class MyService {
private final MyDependency myDependency;
// 构造函数注入依赖
public MyService(MyDependency myDependency) {
this.myDependency = myDependency;
}
public String performAction() {
return myDependency.someMethod();
}
}
// 依赖类
class MyDependency {
public String someMethod() {
return "Real Response";
}
}
public class MyServiceTest {
@Mock
private MyDependency myDependency; // 创建 mock 对象
@InjectMocks
@Spy
private MyService myService; // 自动注入 mock 对象
@Before
public void setUp() {
// 初始化注解标注的对象
MockitoAnnotations.openMocks(this);
}
@Test
public void testPerformAction() {
// 设置 mock 对象的行为
when(myDependency.someMethod()).thenReturn("Mocked Response");
// 调用被测试对象的方法
String result = myService.performAction();
// 验证结果
assertEquals("Mocked Response", result);
}
}
三、实际编写单元测试注意事项
- 在实际测试中,mock 不全,可能会出现空指针情况,这个时候不要慌,先看报错,再根据报错将为空的对象 mock 到测试类中。
- 实际代码中,每个 service 接口方法都可能有多个 return,此时要想保证代码覆盖率,就需要为每个方法构造合适插桩,多写几个 Test 测试,确保覆盖到每个 return。
四、Mockito 在 SpringBoot 环境使用(不推荐,慢)
- @MockBean:类似 @Mock 用于通过类型或名字替换 spring 容器中已经存在的 bean, 从而达到对这些 bean 进行 mock 的目的。
- @SpyBean:作用类似 @Spy 用于通过类型或名字包装 spring 容器中已经存在的 bean,当需要 mock 被测试类的某些方法时可以使用。
- Spring 中生成的对象受 spring 管理,上述 Bean 被 mock 或 spy 后,依然受 Spring 容器管理。