4. Mock基本概念:定义、行为验证、交互验证、与Stub的互补性

Mock 是测试隔离中最重要的替身类型之一。与 Stub 不同,Mock 不仅提供预定义的返回值,还记录并验证被测对象与替身之间的交互——包括方法是否被调用、调用顺序、参数以及调用次数。本章从定义出发,对比行为验证与交互验证,并解释 Mock 与 Stub 如何互补。

4.1 Mock 的定义与角色

Mock 对象是可编程的替身,预先模拟依赖组件的行为,并在测试结束后断言交互是否按预期发生。其核心职责是:

  • 提供响应:像 Stub 一样返回预设值或抛出异常。
  • 记录调用:自动记录每次方法调用的参数、次数、顺序。
  • 执行验证:测试用例在收尾阶段使用验证 API 检查交互的正确性。
特性StubMock
主要目的提供预设响应验证交互行为
是否记录调用不记录(或仅用于状态验证)自动记录全部调用
验证方式断言方法返回值或系统状态断言调用次数、参数、顺序
典型场景隔离外部数据源(数据库、文件)验证日志、通知、外部服务调用是否正确触发

4.2 行为验证 vs 交互验证

两种验证风格在测试中有明确分工:

行为验证(状态验证):执行被测方法后,检查系统状态或返回值。
交互验证(行为验证):验证被测对象是否按预期与协作对象交互——方法被调用了 N 次、携带正确参数、按特定顺序执行。

Mock 专精于交互验证,而 Stub 通常配合行为验证。实际项目中两者经常结合使用:同一替身对象可以既提供 Stub 响应,又作为 Mock 记录交互(例如 Mockito 中的 mock() 默认兼有两种能力)。

4.3 代码示例:使用 Mockito(Java)

假设有一个 OrderService,它依赖 NotificationService 发送确认邮件。我们希望验证下单成功后是否确实调用了 sendConfirmation,并且参数为正确的订单 ID。

// 被测类
public class OrderService {
    private NotificationService notificationService;

    public OrderService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void placeOrder(Order order) {
        // ... 处理订单逻辑
        notificationService.sendConfirmation(order.getId());
    }
}

// 测试类
import static org.mockito.Mockito.*;

public class OrderServiceTest {
    @Test
    public void shouldSendConfirmationWhenOrderPlaced() {
        // 1. 创建 Mock
        NotificationService mockNotification = mock(NotificationService.class);
        OrderService service = new OrderService(mockNotification);
        Order order = new Order(42, "Laptop");

        // 2. 执行被测动作
        service.placeOrder(order);

        // 3. 交互验证:检查 sendConfirmation 被调用且参数为 42
        verify(mockNotification).sendConfirmation(42);
    }
}

上述测试中,Mock 自动记录了 sendConfirmation 的调用情况。如果参数错误或方法未被调用,verify() 会抛出异常,测试失败。

4.4 与 Stub 的互补关系

Mock 和 Stub 不是互斥的,而是互补的工具:

  • 当需要提供数据来驱动被测逻辑时,使用 Stub(返回值、异常)。
  • 当需要确保某条执行路径被正确触发时,使用 Mock(验证调用)。
  • 很多框架(如 Mockito、unittest.mock)允许同一替身同时扮演两个角色。
建议:优先使用 Stub 验证数据正确性,仅在必须验证交互逻辑(如顺序、幂等性、触发次数)时才使用 Mock。过度使用 Mock 会使测试与实现强耦合,降低重构弹性。

4.5 调试技巧与常见错误

问题原因解决
验证失败:预期调用 1 次,实际 0 次被测代码分支未执行到调用点检查条件逻辑,打印 Mock 对象的调用记录(框架提供 getAllInvocations())
验证失败:参数不匹配实际参数与预期参数类型/值不同使用 eq() 或自定义匹配器;检查传参顺序
Mock 方法返回 null 导致空指针未配置 Stub 行为,且该返回值被后续逻辑使用使用 when().thenReturn() 或设置默认答案
过度使用 Mock 导致测试脆弱验证了与业务无关的调用(如日志、getter)仅验证对业务需求有意义的交互,允许内部实现变化

4.6 性能与稳定性优化

  • 避免静态 Mock:Powermock 等静态 Mock 工具会增加测试启动时间,尽量通过依赖注入替换真实对象。
  • 复用 Mock 实例:在测试类的 @BeforeEach 方法中创建 Mock,避免每个测试方法重复构造。
  • 清理验证状态:部分框架(如 Mockito)默认使用严格的验证模式,建议在测试类结尾调用 validateMockitoUsage() 或使用内置的验证钩子。
  • 限制交互验证数量:每个测试方法只验证 1~2 个最关键交互,过多的 verify 会拖慢执行速度并增加维护负担。
注意:Mock 的验证默认是严格的——未声明但实际发生的调用也会导致测试失败(Mockito 的 verifyNoMoreInteractions())。如果不需要此行为,可以在创建 Mock 时使用 withSettings().strictness(Strictness.LENIENT)。
Mock 交互验证流程 Mock 交互验证流程图 被测对象 Mock 对象 测试用例 调用方法 记录所有调用 自动 验证(次数, 参数) 通过/失败 被测对象不直接调用真实依赖 而是与 Mock 协作;测试验证交互

4.7 小结