第3章 Mockito基本使用:@Mock、@InjectMocks、@Spy、@Captor注解及MockitoAnnotations.openMocks
3.1 核心注解概述
Mockito 提供一组声明式注解,用于简化 mock 对象的创建、注入和参数捕获。本章聚焦四个最常用的注解及其初始化方式,帮助开发者在 Worker 单元测试中快速搭建 mock 环境。
| 注解 | 用途 | 适用场景 |
|---|---|---|
@Mock | 创建 mock 对象,所有方法返回默认值 | 被测试类依赖的外部服务、DAO、工具类 |
@InjectMocks | 自动将 @Mock 或 @Spy 对象注入到被测试实例 | 被测试类(如 Worker)内部依赖的自动装配 |
@Spy | 创建 spy(部分 mock),保留真实方法行为,可选择性 stub | 需要保留部分真实逻辑的复杂对象 |
@Captor | 在验证阶段捕获方法调用时传入的参数 | 断言内部调用参数是否符合预期 |
3.2 注解初始化方式
使用这些注解前必须调用初始化方法,否则注解不会生效。三种主流初始化方式:
- MockitoAnnotations.openMocks(this) —— 推荐方式,返回
AutoCloseable,可在@BeforeEach中调用。 - MockitoRule —— JUnit 4 风格,通过
@Rule自动初始化。 - MockitoExtension —— JUnit 5 官方扩展,通过
@ExtendWith声明。
实践建议: 在 JUnit 5 项目中优先使用
MockitoExtension;若需手动控制生命周期,使用 MockitoAnnotations.openMocks(this) 并在 @AfterEach 中 close()。
3.3 代码示例:Worker 测试中的注解使用
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class WorkerServiceTest {
@Mock
private TaskRepository taskRepository;
@Mock
private NotificationClient notificationClient;
@InjectMocks
private WorkerService workerService;
@Captor
private ArgumentCaptor<Task> taskCaptor;
@Test
void testProcessTask_success() {
// given
Task inputTask = new Task("ID-001", "compile");
Mockito.when(taskRepository.findById("ID-001")).thenReturn(inputTask);
// when
workerService.processTask("ID-001");
// then – 验证通知发送并捕获参数
Mockito.verify(notificationClient).sendNotification(taskCaptor.capture());
Task capturedTask = taskCaptor.getValue();
Assertions.assertEquals("compile", capturedTask.getAction());
}
}
3.4 注解交互流程
下图展示了 @InjectMocks 如何将 @Mock 和 @Spy 注入到被测试对象,以及 @Captor 在验证阶段捕获参数的完整数据流:
3.5 调试与验证技巧
- 验证注解是否被初始化: 在
@BeforeEach或测试方法中加断点,观察taskRepository是否为 mock 实例(Mockito.mock(TaskRepository.class))。 - 检查注入失败: 如果
@InjectMocks注入失败,Mockito 会抛出MockitoException,通常提示“Could not inject mocks”。检查被测试类的构造函数或 setter 是否匹配 mock 类型。 - 使用
Mockito.validateMockitoUsage(): 在测试类 tearDown 中调用一次,可检测未使用的 stub 或未完成的验证。
3.6 常见错误及解决
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
MockitoException: Cannot create mock for class | 目标类为 final / 不可 mock | 使用 Mockito.mock(…, Mockito.withSettings().lenient()) 或升级 Mockito 版本 |
@InjectMocks 注入 null | 被测试类没有无参构造,且 mock 类型不匹配 | 显式添加构造器注入或字段注入的 setter |
ArgumentCaptor 捕获时 ClassCastException | 泛型擦除导致类型不匹配 | 使用 @Captor 注解时必须声明泛型类型,且 verify 时使用 captor.capture() |
| 注解不生效,mock 对象为 null | 未调用 MockitoAnnotations.openMocks(this) 或未添加 @ExtendWith | 检查 JUnit 版本并正确配置初始化方式 |
性能提示: 在每个测试方法前初始化大量 mock 对象会影响速度。如果测试类中有超过 5 个
@Mock,考虑使用 MockitoSession 或只实例化必要的 mock。同时避免在 @BeforeEach 中做过多的 stub 设置(尤其是数据库调用),推荐每个测试方法只 stub 当前用例所需的方法。
3.7 最佳实践与稳定性建议
- 优先使用
@ExtendWith(MockitoExtension.class): 它会在每个测试方法执行前自动打开 mock,并在方法结束后清理,无需手动close()。 - 避免过度使用
@Spy: spy 会保留真实行为,容易因 side effect 导致测试不稳定。能用@Mock替代的场景尽量用 mock。 - 每个测试只使用一个
@Captor: 捕获多个参数时使用多个@Captor实例,不要重复使用同一个 captor。 - 显式声明
Mockito.lenient(): 对于不会被所有测试方法调用的 stub,使用lenient()避免UnnecessaryStubbingException。