4. 基于Mock框架的Stub:Mockito/PowerMock用法、行为录制、验证
在测试模块边界时,手动编写Stub类虽然灵活,但当接口数量膨胀或依赖行为复杂时,维护成本急剧上升。Mockito与PowerMock作为Java生态最主流的Mock框架,提供了声明式的行为录制与验证机制,将Stub的创建、管理与测试逻辑解耦。本章聚焦于这两种框架的核心API,覆盖从环境配置到验证告警的全流程。
前置说明: Mockito 2.x+ 已内置对final类/方法的支持,而PowerMock主要用于静态方法、构造器、私有方法的模拟。现代项目中优先推荐Mockito + 少量PowerMock的组合。
4.1 核心工作流:录制 → 执行 → 验证
基于Mock框架的Stub操作严格遵循以下三步流程。下图展示了Mockito/PowerMock在模块边界测试中的通用协作模型:
4.2 Mockito核心API详解
4.2.1 基本Stub录制:when().thenReturn()
这是Mockito最常用的Stub定义方式。当被测代码调用某个mock对象的方法时,返回预设值。
// 示例:模拟UserRepository接口
UserRepository mockRepo = mock(UserRepository.class);
// 录制:当findById(42)被调用时,返回预定义的User对象
User expectedUser = new User(42, "Alice");
when(mockRepo.findById(42)).thenReturn(expectedUser);
// 录制:当findById(999)被调用时,抛出异常
when(mockRepo.findById(999)).thenThrow(new UserNotFoundException("User 999 not found"));
// 录制:链式调用,第一次返回null,第二次返回对象
when(mockRepo.findById(1))
.thenReturn(null)
.thenReturn(new User(1, "Bob"));
| 方法 | 用途 | 典型场景 |
|---|---|---|
thenReturn(value) | 固定返回值 | 查询类方法 |
thenThrow(exception) | 抛出异常 | 异常路径测试 |
thenAnswer(Answer) | 动态计算返回值 | 基于参数生成返回对象 |
thenCallRealMethod() | 调用真实方法 | 部分模拟 (Spy) |
4.2.2 参数匹配器
当不关心或需要灵活匹配参数时,使用Matcher族方法。
// 任意参数
when(mockRepo.findById(anyInt())).thenReturn(defaultUser);
when(mockRepo.save(any(User.class))).thenReturn(true);
// 特定范围
when(mockRepo.findById(eq(100))).thenReturn(specialUser);
when(mockRepo.findById(gt(1000))).thenThrow(new IllegalArgumentException());
// 自定义匹配
when(mockRepo.findById(argThat(id -> id > 0 && id < 100))).thenReturn(smallIdUser);
// 注意:如果使用Matcher,所有参数都必须使用Matcher
// 错误示范:when(mockRepo.findById(1, anyString())).thenReturn(...) // 编译错误
4.2.3 行为验证:verify()
验证(Verification)是Mockito区别于普通Stub的关键特性。它检查mock对象的方法是否按照预期被调用。
// 基础验证
verify(mockRepo).findById(42); // 默认验证一次
verify(mockRepo, times(2)).save(any()); // 验证调用次数
verify(mockRepo, never()).delete(any()); // 验证从未调用
verify(mockRepo, atLeastOnce()).findById(anyInt());
verify(mockRepo, atMost(3)).flush(); // 最多3次
// 验证顺序
InOrder inOrder = inOrder(mockRepo, mockCache);
inOrder.verify(mockRepo).findById(42);
inOrder.verify(mockCache).evict(42);
// 验证超时
verify(mockRepo, timeout(100).times(2)).process(); // 100ms内调用2次
常见误用: 在验证时使用
verify(mockRepo).findById(anyInt())会忽略具体参数。如果业务逻辑依赖特定参数值,应使用具体的值(如42)或精确的Matcher(如eq(42)),避免漏测。
4.3 PowerMock:突破静态与构造方法限制
当被测类依赖静态方法、new操作符或私有方法时,标准Mockito无法直接模拟。PowerMock通过字节码操作(如javassist)在类加载阶段修改字节码,从而支持对这些特殊方法的Stub。
4.3.1 配置准备
// 1. 添加依赖 (Maven)
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
// 2. 测试类注解
@RunWith(PowerMockRunner.class)
@PrepareForTest({SystemUtils.class, FileUtil.class}) // 需要增强的类
public class BackupServiceTest { ... }
4.3.2 静态方法Stub
// 模拟 SystemUtils.isWindows() 返回 false
PowerMockito.mockStatic(SystemUtils.class);
when(SystemUtils.isWindows()).thenReturn(false);
// 模拟 FileUtil.createDirectory(anyString()) 返回 false
PowerMockito.mockStatic(FileUtil.class);
when(FileUtil.createDirectory(anyString())).thenReturn(false);
// 验证静态方法调用
PowerMockito.verifyStatic(SystemUtils.class, times(1));
SystemUtils.isWindows();
4.3.3 new对象拦截
// 假设被测类内部有: File backupFile = new File("/tmp/backup.zip")
File mockFile = PowerMockito.mock(File.class);
when(mockFile.exists()).thenReturn(true);
when(mockFile.delete()).thenReturn(true);
// 录制:当new File("/tmp/backup.zip")时返回mockFile
PowerMockito.whenNew(File.class)
.withArguments("/tmp/backup.zip")
.thenReturn(mockFile);
// 执行被测方法
target.backup();
// 验证new是否被调用
PowerMockito.verifyNew(File.class)
.withArguments("/tmp/backup.zip");
性能注意事项: PowerMock的
@PrepareForTest会导致JVM为指定类重新加载字节码,每个测试类只应包含最必要的增强类。过多使用PowerMock会显著降低测试套件执行速度,建议仅在必须绕过私有/静态方法时使用。
4.4 行为录制的最佳实践
- 明确的Stub范围: 只录制被测试类直接调用的依赖方法,避免过度录制不相关的内部方法,否则会导致测试与实现过度耦合。
- 避免魔数: 录制时使用的参数和返回值应提取为有意义的测试常量和变量,便于阅读和维护。
- 链式录制需谨慎:
thenReturn(a).thenReturn(b)要求调用方严格按顺序调用,对后续重构不友好。推荐在必要时使用thenAnswer()或简单写出多次调用。 - Spy vs Mock: 当大部分方法要调用真实逻辑,仅个别方法需要Stub时,使用
spy()而非mock()。
// Spy 示例:部分模拟
UserService realService = new UserService();
UserService spyService = spy(realService);
// 仅录制 getPermission() 方法
doReturn("ADMIN").when(spyService).getPermission(anyString());
// 其他方法调用真实实现
spyService.login("alice"); // 实际调用 realService.login()
4.5 常见错误与调试
| 错误类型 | 典型现象 | 解决方案 |
|---|---|---|
| UnnecessaryStubbingException | 录制的Stub从未被调用 | 移除未使用的录制,或调整测试逻辑使其被触发 |
| TooLittleActualInvocations | verify发现调用次数不足 | 检查被测代码是否真的调用了该Mock方法;检查参数匹配是否严格 |
| MissingMethodInvocation | 在录制块内部调用了非Mock方法 | 确保when().thenReturn()的when()内部必须是Mock方法调用 |
| PowerMock类加载失败 | NoClassDefFoundError 或 ClassFormatError | 确认@PrepareForTest包含所有需要字节码增强的类(包括父类) |
| Mixed Matchers | 一个参数用Matcher,其他用精确值 | 所有参数要么都用Matcher,要么都不用。使用eq()包裹精确值 |
4.6 性能与稳定性优化
- 最小化PowerMock作用域: 将PowerMockRunner限制在不使用静态/构造模拟的子类中,使用MockitoJUnitRunner处理纯接口模拟。
- 避免在循环中创建Mock: Mock对象的创建有开销。对于大量独立测试,优先使用
@Mock注解+initMocks()统一初始化。 - 使用
@InjectMocks自动注入: 可以自动将模拟依赖注入到被测类,减少样板代码,但要注意字段类型必须匹配。 - 静态导入核心方法:
import static org.mockito.Mockito.*;和import static org.powermock.api.mockito.PowerMockito.*;能清晰表达意图。 - 日志辅助调试: 在调试阶段可以使用
Mockito.debug()或开启框架日志(-Dorg.mockito.debug=true)查看Mock调用记录。
// 推荐的测试类骨架
@RunWith(MockitoJUnitRunner.class)
public class OrderServiceTest {
@Mock
private InventoryClient inventoryClient; // 远程接口Mock
@Mock
private PaymentGateway paymentGateway; // 支付网关Mock
@InjectMocks
private OrderService orderService; // 被测类,自动注入以上Mock
@Test
public void testPlaceOrder_Success() {
// 录制
when(inventoryClient.checkStock("SKU-001", 2)).thenReturn(true);
when(paymentGateway.charge(anyString(), eq(199.99))).thenReturn("TXN-001");
// 执行
Order result = orderService.placeOrder("user-1", "SKU-001", 2);
// 验证
verify(inventoryClient).checkStock("SKU-001", 2);
verify(paymentGateway).charge("user-1", 199.99);
assertEquals("TXN-001", result.getTransactionId());
}
}
调试技巧: 若测试失败且原因不明,可在
@After方法中添加validateMockitoUsage()(Mockito 1.x)或直接使用Mockito 2.x的默认校验。对于PowerMock,可以在JVM参数中添加-XX:+TraceClassLoading观察类加载顺序。