1.1 Mock 概念与核心价值

什么是 Mock

Mock 是测试领域中用于模拟真实依赖对象的一种技术手段。在单元测试中,被测试单元(SUT, System Under Test)往往依赖外部组件——数据库、远程服务、文件系统、消息队列等。Mock 对象通过预设行为和验证交互,让测试完全聚焦于被测逻辑,而不受外部环境干扰。

核心定义:Mock 是“带行为预设与交互验证的替身对象”。它不仅能返回预设值,还能记录调用次数、参数顺序,并在测试结束时断言这些交互是否符合预期。

Mock 在单元测试中的作用

  1. 隔离依赖——切断被测代码与外部服务、数据库、文件系统的直接连接,使测试不依赖环境可用性。
  2. 加速执行——Mock 对象在内存中运行,毫秒级返回,避免网络 I/O 或磁盘读写带来的延迟。
  3. 模拟边界——轻松制造超时、异常、空返回值等真实依赖难以触发的场景。
  4. 验证交互——确认被测代码是否按预期调用了依赖方法、传递了正确参数、控制了调用次数。
  5. 并行测试——消除共享资源竞争,使测试用例可独立、并行执行。
作用维度 无 Mock 的痛点 使用 Mock 后的效果
环境依赖 测试必须连接真实数据库/服务 完全脱离外部环境,本地即可运行
执行速度 单用例可能耗时数秒(含网络/磁盘) 毫秒级返回,千级用例秒级完成
异常覆盖 难以稳定触发网络超时、服务 500 一行代码即可抛出任意异常
交互验证 只能通过最终结果间接推断 直接断言调用次数、参数、顺序
实践建议:Mock 应仅用于“外部不可控”的依赖——第三方 API、数据库、消息中间件。对于同进程内的值对象或纯函数,直接使用真实对象更简单可靠。

Mock 与 Stub / Fake 的区别

测试替身(Test Double)家族包含多种角色,Mock、Stub、Fake 是最常混淆的三类。它们的核心差异在于验证目标和实现复杂度。

维度 Mock Stub Fake
核心目的 验证交互行为 提供预设返回值 提供轻量级功能实现
是否验证调用 是——断言调用次数、参数、顺序 否——只返回数据,不关心是否被调用 否——提供真实逻辑,但不验证调用
实现复杂度 中——需框架支持行为预设+验证 低——通常返回固定值或简单逻辑 高——需实现完整接口的简化版
典型场景 验证支付服务是否被调用一次 让 UserService.getUser() 返回预设用户 用内存 Map 实现真实数据库接口
状态验证 否——关注行为而非状态 是——关注返回结果是否正确 是——关注简化逻辑是否正确
常见误区:很多开发者把所有测试替身都称为“Mock”,导致测试意图混乱。记住一条黄金法则:验证交互用 Mock,提供数据用 Stub,替代实现用 Fake。混用会使测试难以理解和维护。

三种替身的关系对比图

测试替身家族:Mock vs Stub vs Fake Test Double(测试替身) Mock Stub Fake 核心特征 ✅ 行为预设 ✅ 交互验证 ❌ 真实逻辑 核心特征 ✅ 预设返回值 ❌ 交互验证 ❌ 真实逻辑 核心特征 ✅ 简化功能实现 ❌ 交互验证 ✅ 可运行逻辑 // Mock: 验证 paymentService.pay() 被调用一次 verify(paymentMock, times(1)).pay(any()); // Stub: 返回固定用户 // Fake: 内存数据库实现 when(userRepo.findById(1)).thenReturn(user); class InMemoryDB implements DB { ... }

代码对比:Mock vs Stub vs Fake

以下示例展示同一接口 PaymentGateway 在三种替身下的不同用法:

// 接口定义
public interface PaymentGateway {
    boolean charge(String userId, double amount);
}

// ========== Stub:只返回预设值,不验证调用 ==========
public class PaymentGatewayStub implements PaymentGateway {
    private final boolean result;
    public PaymentGatewayStub(boolean result) { this.result = result; }
    @Override
    public boolean charge(String userId, double amount) {
        return result;  // 固定返回 true 或 false
    }
}

// ========== Fake:简化但可运行的实现 ==========
public class PaymentGatewayFake implements PaymentGateway {
    private final Map<String, Double> balances = new HashMap<>();
    @Override
    public boolean charge(String userId, double amount) {
        Double bal = balances.getOrDefault(userId, 0.0);
        if (bal >= amount) {
            balances.put(userId, bal - amount);
            return true;
        }
        return false;
    }
}

// ========== Mock:使用 Mockito 框架,预设行为 + 验证交互 ==========
@Test
public void testOrderService() {
    PaymentGateway mockGateway = mock(PaymentGateway.class);
    when(mockGateway.charge("user1", 100.0)).thenReturn(true);

    OrderService service = new OrderService(mockGateway);
    boolean result = service.placeOrder("user1", 100.0);

    assertTrue(result);
    // 验证 charge 方法被调用且参数正确
    verify(mockGateway, times(1)).charge("user1", 100.0);
}
选型指南:
  • 需要验证“是否调用了某方法”或“调用了几次”→ 用 Mock
  • 只需要依赖返回特定值来推进测试逻辑 → 用 Stub
  • 需要一个轻量级但可运行的替代实现(如内存数据库)→ 用 Fake

Mock 的核心价值总结

  • 隔离性——测试只关注被测代码,不因外部服务宕机而失败。
  • 确定性——每次运行结果一致,不受网络波动或数据变化影响。
  • 可测性——覆盖真实环境难以构造的异常、超时、边界条件。
  • 可维护性——测试与外部实现解耦,依赖接口变更时只需调整 Mock 预设。
一句话记住:Mock 是“带行为预设与交互验证的替身”,Stub 是“只提供数据的替身”,Fake 是“可运行的简化替身”。三者在测试金字塔中各司其职,合理搭配才能写出健壮、快速、可维护的单元测试。