### 第4章 基于Mock框架的Stub:Mockito/PowerMock用法、行为录制、验证

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在模块边界测试中的通用协作模型:

Mockito/PowerMock 行为录制与验证流程 ① 录制预期行为 when(mock.method()).thenReturn(x) ② 调用被测代码 被测类调用mock依赖 ③ 验证交互 verify(mock).method() Mock对象生命周期 创建Mock → 录制 (Stubbing) → 执行 → 验证 → 重置 (可选) PowerMock扩展 mockStatic(Class) whenNew(Class) 模块边界 被测模块 ← Mock → 外部依赖 接口契约: Interface/Abstract

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从未被调用移除未使用的录制,或调整测试逻辑使其被触发
TooLittleActualInvocationsverify发现调用次数不足检查被测代码是否真的调用了该Mock方法;检查参数匹配是否严格
MissingMethodInvocation在录制块内部调用了非Mock方法确保when().thenReturn()的when()内部必须是Mock方法调用
PowerMock类加载失败NoClassDefFoundError 或 ClassFormatError确认@PrepareForTest包含所有需要字节码增强的类(包括父类)
Mixed Matchers一个参数用Matcher,其他用精确值所有参数要么都用Matcher,要么都不用。使用eq()包裹精确值

4.6 性能与稳定性优化

  1. 最小化PowerMock作用域: 将PowerMockRunner限制在不使用静态/构造模拟的子类中,使用MockitoJUnitRunner处理纯接口模拟。
  2. 避免在循环中创建Mock: Mock对象的创建有开销。对于大量独立测试,优先使用@Mock注解+initMocks()统一初始化。
  3. 使用@InjectMocks自动注入: 可以自动将模拟依赖注入到被测类,减少样板代码,但要注意字段类型必须匹配。
  4. 静态导入核心方法: import static org.mockito.Mockito.*;和import static org.powermock.api.mockito.PowerMockito.*;能清晰表达意图。
  5. 日志辅助调试: 在调试阶段可以使用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观察类加载顺序。