Mock 基础:你其实每天都在用,只是没意识到

做 AOSP 测试,绕不开 Mock。说白了,Mock 就是给被测代码找个“替身演员”。

但替身也分好几种。有的只负责站那儿不说话,有的得按剧本演,有的要偷偷记下你做了啥动作。咱们做测试框架,得先搞清楚到底该用哪种。

我个人习惯把 Mock 分为四类:Stub、Spy、Fake、Dummy。别被名字吓到,它们背后逻辑其实很朴素。

1. Dummy:最省事的“假参数”

Dummy 是最轻量级的东西。它本身不干活,也不返回值。它的存在,只是为了把方法签名填满。

我举个例子。假设有个接口:

public interface OnSensorChangedListener {
    void onSensorChanged(SensorEvent event, Context context);
}

你测试某个类时,必须传入这个监听器。但测试中你根本不在意它回调了什么。那就扔个 Dummy 进去:

OnSensorChangedListener dummy = (e, c) -> {};

特点:只占位、无行为、不验证。

注意:Dummy 不能为 null。 如果你传 null,代码里可能 NPE 提前崩了,测不出你想要的东西。我踩过这个坑——在测试某 AOSP 的 PowerManager 服务时,传了个 null 的对象过去,结果还没测到关键路径就挂了。

Dummy 的用处比较狭窄,但也是个基础认知。特别是给接口参数多的老 API 写测试,这个技巧很实用。

2. Stub:给代码喂“固定答案”

Stub 比 Dummy 高级一点。它会返回值,但值是事先定死的。说白了,你问它“今天下雨吗”,它永远回答“下”。

看个真实例子:

// 被测方法
public boolean isLargeScreen(Display display) {
    return display.getWidth() > 1024;
}

// Stub Display
Display stubDisplay = new Display() {
    @Override
    public int getWidth() {
        return 1440;  // 固定返回大屏宽度
    }
};

这种 Stub 在测试中特好用。你只需要控制某个输入,就能让被测代码走你想要的逻辑分支。

核心要点:

  • Stub 只管“给答案”,不管“问了几次”
  • 它可以为同一个方法返回不同的值(比如设置参数来切换返回)
  • 不要用 Stub 验证行为——它不记录调用次数

我记得有次在测试 AOSP 的 ActivityManagerService 里的权限判断逻辑,我就用 Stub 把某个系统服务的返回值固定为 true,逼着代码走通过权限的流程。效果立竿见影。

3. Spy:既想知道做了什么,又想保留真实逻辑

Spy 是个“半真半假”的东西。它默认走真实行为,但你可以随时替换部分行为。

我曾经在测试 AOSP 中的 AlarmManager 时遇到一个场景:我想保留它的真实定时逻辑,但又不希望报警真的响起来。这就是 Spy 的典型用法。

// 创建 Spy 对象
AlarmManager realAlarm = new AlarmManager(context);
AlarmManager spy = Mockito.spy(realAlarm);

// 只把这个方法“替换”掉
doNothing().when(spy).setRepeating(anyInt(), anyLong(), anyLong(), any());

// 其他方法走真实逻辑
spy.cancel(pendingIntent);  // 真实 cancel
spy.setRepeating(...);      // 什么也不做

Spy 的价值:它是真实对象和 Mock 对象之间的桥梁。你不想重构代码时,Spy 是最轻的介入方式。

**避坑点**:Spy 默认调用真实方法。如果真实方法有副作用(如写文件、发送广播),你要提前确认能不能接受。我曾经在测试某个 AOSP 的电源管理模块时,用 Spy 保留了真实写日志逻辑,结果测试跑完后单元测试目录里多了一堆日志文件。

4. Fake:轻量的“真实实现”

Fake 最有意思。它是个能运行的“假货”。和 Stub 不同,Fake 内部有完整的逻辑,只是实现方式比真实的简单、轻量。

最经典的例子是 Fake 数据库。真实数据要 SQLite 或 Room,Fake 可以就存一个 HashMap:

public class FakeUserRepository implements UserRepository {
    private Map<String, User> store = new HashMap<>();

    @Override
    public void save(User user) {
        store.put(user.getId(), user);  // 真实逻辑:存数据库
    }

    @Override
    public User findById(String id) {
        return store.get(id);  // 真实逻辑:查数据库
    }
}

这个 Fake 内部有状态、有逻辑,跑起来和真的一样,只是不用依赖底层存储框架。换句话说,Fake 是可执行的单元测试替身,不是纯 Stub。

Fake 在 AOSP 测试里非常普遍。比如你在测某个系统服务时,不想依赖真机 SensorService,就可以写个 Fake 版本。它跑得贼快,也不受硬件状态影响。

适用场景:

  • 测试时需要反复查数据、改数据
  • 不想依赖真实外部设备(网络、数据库、外设)
  • 需要验证多个交互流程

四种 Mock 类型一览

为了方便你对照,我整理了一个表格:

类型 有返回值吗? 有内部逻辑吗? 记录行为吗? 典型场景
Dummy 否 否 否 填充参数
Stub 是(固定值) 否 否 控制返回值、驱动分支
Spy 是(默认真实) 是(可覆盖) 是 部分 Mock、保留真实行为
Fake 是(逻辑计算) 是(简化版) 否 轻量实现替代真实依赖

SVG:四种 Mock 的层级与关系

四种 Mock 类型层级关系 Mock 测试替身 Dummy Stub Spy Fake 仅占位 无返回值 不记录行为 固定返回值 无内部逻辑 不记录调用 部分替换行为 保留真实逻辑 记录调用次数 简易实现 有内部状态 可独立运行 复杂度递增 Dummy → Stub → Spy → Fake

怎么选?一个小建议

你不可能每个测试都用 Fake。那样太重了。

我自己的原则是:

  • 只需要一个空参数 → Dummy
  • 只控制返回值来走分支 → Stub
  • 想保留大部分真实行为、只替换一小部分 → Spy
  • 需要完整流程、但不能依赖真机环境 → Fake

举个例子,在 AOSP 测试里:

  • 测某个系统服务启动顺序时,我常用 Stub 把某些模块的返回值提前固定
  • 测绑定服务的生命周期时,用 Spy 保留真实 bind 行为、只 Mock 掉 onServiceConnected
  • 测 AIDL 接口的多步交互时,用 Fake 手写一个简易服务端
**小贴士**:优先用 Stub 和 Spy,因为它们实现最快。Fake 虽然好,但有维护成本。Dummy 最便宜,但别忘了,它不能为 null。

好了,以上就是四种 Mock 的基础概念。