1.1 Mock 概念与核心价值
什么是 Mock
Mock 是测试领域中用于模拟真实依赖对象的一种技术手段。在单元测试中,被测试单元(SUT, System Under Test)往往依赖外部组件——数据库、远程服务、文件系统、消息队列等。Mock 对象通过预设行为和验证交互,让测试完全聚焦于被测逻辑,而不受外部环境干扰。
核心定义:Mock 是“带行为预设与交互验证的替身对象”。它不仅能返回预设值,还能记录调用次数、参数顺序,并在测试结束时断言这些交互是否符合预期。
Mock 在单元测试中的作用
- 隔离依赖——切断被测代码与外部服务、数据库、文件系统的直接连接,使测试不依赖环境可用性。
- 加速执行——Mock 对象在内存中运行,毫秒级返回,避免网络 I/O 或磁盘读写带来的延迟。
- 模拟边界——轻松制造超时、异常、空返回值等真实依赖难以触发的场景。
- 验证交互——确认被测代码是否按预期调用了依赖方法、传递了正确参数、控制了调用次数。
- 并行测试——消除共享资源竞争,使测试用例可独立、并行执行。
| 作用维度 | 无 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
以下示例展示同一接口 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 是“可运行的简化替身”。三者在测试金字塔中各司其职,合理搭配才能写出健壮、快速、可维护的单元测试。