3. Mock基础理论:Mock对象、Stub、Spy、Fake的区别与选择
关于Mock、Stub、Spy、Fake这几个词,很多人刚接触时容易搞混。我记得刚入行那会儿,团队里讨论测试方案,同事说“我们mock一下数据库”,结果他写了个返回固定数据的对象——严格来说那其实是个Stub。这种概念混淆在真实项目里挺常见的。所以这一节,咱们就把它们掰开揉碎,说清楚。
3.1 什么是测试替身(Test Double)?
这四个家伙其实都属于“测试替身”家族。说白了,就是用假的对象代替真的组件,让测试能独立、快速、可控地运行。我习惯把测试替身分为两类:一类是用来验证行为的,另一类是用来提供数据的。Mock和Spy偏向前者,Stub和Fake偏向后者。但实际用起来,边界没那么。
3.2 逐一拆解:Mock、Stub、Spy、Fake
3.2.1 Mock对象
Mock的核心:验证交互行为。 你创建一个Mock对象,告诉它:“等一下会有人调用 doSomething() 方法,参数是X,返回Y”。然后执行被测代码,最后验证那个方法是否真的被调用了。这就是行为验证(behavior verification)。
举个例子(Java + Mockito风格)
// 创建 Mock
UserService mockService = mock(UserService.class);
when(mockService.getUser(1)).thenReturn(new User("Alice"));
// 执行被测逻辑
UserController controller = new UserController(mockService);
String result = controller.getUserName(1);
// 验证行为 —— 这是 Mock 的专属步骤
verify(mockService).getUser(1);
我个人习惯在需要严格保证调用顺序或次数时用Mock。比如要验证某个缓存失效后是否重新加载了数据。但要注意:别过度验证,否则测试和实现耦合得太死,改一行代码就要改测试。
3.2.2 Stub
Stub的核心:提供预设的返回值。 它不关心方法被调了几次,也不记录调用信息。你只需要告诉它“当调用这个方法时,返回这个值”。
举个例子,你的代码依赖一个外部天气API,你写个Stub固定返回“晴天”,这样就不用真的去联网。Stub通常没有验证能力——如果测试结束时你不检查调用情况,那它就是Stub。
避坑指南
我曾经在项目里把一个Stub当Mock用,在Stub上调用verify(),结果抛异常,查了半天才发现那个库不支持。后来我养成习惯:用框架区分这两个角色。Mockito里 mock() 可以当Stub用,但 @Mock 对象也可以做verify;而如果纯粹只想返回数据,我会用 Answer 或者直接创建匿名类。
3.2.3 Spy
Spy是真实对象的外壳。 它底层走真实逻辑,但你可以对某些方法进行部分mock(partial mocking)。更关键的是,Spy会记录所有真实调用的信息,之后可以像Mock一样验证。
// Spy 示例
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);
spyList.add("one"); // 真实调用
spyList.add("two");
verify(spyList, times(2)).add(anyString()); // 验证通过了
你在项目里可以用Spy来测试那些“大部分都要跑真实代码,只改一两个方法”的场景。比如你的AOSP组件里有个Handler,你想保留它的消息循环逻辑,但把网络请求替换掉。用Spy就很方便。
3.2.4 Fake
Fake是轻量级的真实实现。 它和Stub最大的区别是:Fake通常包含业务逻辑——只是比原实现简单。比如用内存Map模拟数据库,支持增删改查,但不上磁盘。这就是Fake。而Stub可能只返回固定数据,不实现完整的CRUD。
Fake vs Stub 对比
// Stub:只返回固定数据
User getUser(int id) { return new User("固定用户"); }
// Fake:实现了功能,但简化
class InMemoryUserRepository implements UserRepository {
private Map<Integer, User> store = new HashMap<>();
public User getUser(int id) { return store.get(id); }
public void save(User u) { store.put(u.id, u); }
}
我在AOSP的测试实践中,经常给ContentProvider或SharedPreferences写Fake。比如一个内存版的SharedPreferences,读完可以写,还支持commit/apply的模拟。这种Fake跑测试比用Mock快,而且可维护性更好——因为行为接近真实。
3.3 一张表看懂区别
| 特性 | Mock | Stub | Spy | Fake |
|---|---|---|---|---|
| 是否验证行为 | ✅ 是 | ❌ 否 | ✅ 是 | ❌ 否(但可验证状态) |
| 是否返回预设值 | ✅ 可配置 | ✅ 必须配置 | ✅ 可部分配置 | ❌ 通常自行实现 |
| 是否使用真实逻辑 | ❌ 否 | ❌ 否 | ✅ 大部分真实 | ✅ 轻量真实逻辑 |
| 典型场景 | 验证调用顺序/次数 | 隔离外部依赖 | 部分mock + 记录 | 替代数据库/文件系统 |
| 实现复杂度 | 中(需框架) | 低 | 中(需框架) | 高(需写实现) |
3.4 在实际项目中怎么选?
我自己有个简单原则:
- 只想替换掉依赖、不关心交互细节 → 用Stub。最简单,代码量最少。
- 需要验证某个方法是否被正确调用 → 用Mock。比如注册监听器后是否被回调。
- 大部分用真实实现,只替换一两个方法 → 用Spy。省得写整个Fake。
- 需要模拟的功能有复杂状态逻辑(比如缓存、数据库) → 用Fake。虽然写起来麻烦,但长期维护成本低。
注意:不要混用术语导致误解
我曾经在代码评审里看到有人把Mock和Stub混着叫,结果新同事理解错了,写了一个既没有verify、返回值也不对的测试,折腾了两天。建议团队统一说法:你写的是Mock还是Stub,评审时讲清楚。用Mock框架但没写verify,那就是Stub——别不好意思,写对就行。
3.5 AOSP Mock回放中的角色定位
在AOSP组件测试里,这四个角色各有用途:
- Mock:用于验证系统服务的调用期望(如Binder IPC次数)。
- Stub:快速替代SensorManager、NetworkManager等外部依赖,返回默认值。
- Spy:包裹真实的Handler或Looper,保留框架行为,只拦截部分方法做异步控制。
- Fake:写内存版的PackageManager或ContentProvider,让测试可以insert/query,而不依赖真实设备。
说白了,你不需要在每个测试里都严格区分它们——工具是为你服务的。但理解这些本质区别,能帮你写出更干净、更稳定的测试代码。毕竟,好的测试不该在真假之间糊里糊涂。