Mock基础:Mock对象、Stub、Verify等概念

说实话,很多人刚接触Mock时,容易被各种术语绕晕。Mock、Stub、Spy、Verify……这些到底有啥区别?我当年第一次写单元测试,对着Mockito文档看了半天,脑子里就一句话——这玩意儿到底怎么用?

后来踩了不少坑,才总结出一个经验:你不需要一次性记住所有概念,只需要搞懂三样东西——Mock对象、Stub行为、Verify验证。其他所有高级玩法,都是这三样组合出来的。

一、Mock对象到底是什么?

说白了,Mock对象就是一个“替身演员”。我们在测试中不想依赖真实组件(比如网络请求、数据库、硬件接口),就用Mock对象替换掉它们。

为什么要这么做?举个例子:你在做AOSP的SystemUI测试,想验证“点击某个按钮后,亮度条是否被正确更新”。但真实环境下,亮度调节依赖硬件驱动。如果你用真实设备,每次测试都得等硬件响应,慢得要命,而且可能产生副作用(比如屏幕真的变暗了)。

这时候,Mock对象就派上用场了。它模拟了真实对象的行为,但不需要实际硬件参与。测试跑得飞快,而且完全可控。

关键点:Mock对象 ≠ 真实对象。它只是一个空壳,默认所有方法返回默认值(null、0、false等)。你需要手动告诉它“当这个方法被调用时,应该返回什么”。

在AOSP项目中,我们常用的Mock框架是Mockito(Java/Kotlin)和它的Android扩展。比如这样创建一个Mock对象:

// 创建一个Mock的BrightnessController
BrightnessController mockController = Mockito.mock(BrightnessController.class);

嗯,注意这里没有使用真实构造器,Mockito会直接创建一个代理对象。我在项目中曾犯过一个错误:试图用new BrightnessController()然后手动设置Mock,结果发现真实构造器里有一堆初始化逻辑,根本绕不过去。正确做法就是直接用mock()。

二、Stub:让Mock对象“说人话”

Mock对象本身是个哑巴。你调用它的任何方法,它都返回默认值。我需要它根据特定输入返回特定结果,这就是Stub(桩)。

打个比方:Mock对象是话务员,Stub就是话务员手里的脚本。客户问“今天天气怎么样”,话务员就照脚本回答“晴天”。

代码上怎么实现?用when().thenReturn():

// 当调用mockController.getBrightnessLevel()时,返回50
Mockito.when(mockController.getBrightnessLevel()).thenReturn(50);

// 也可以根据参数返回不同结果
Mockito.when(mockController.setBrightness(100)).thenReturn(true);
Mockito.when(mockController.setBrightness(-1)).thenReturn(false);
一个小经验:Stub时注意参数匹配。如果你写when(x.setBrightness(anyInt())),意味着任何int都匹配。但如果你写了具体的100,只有传入100才命中。我曾经因为参数写错,Stub死活不生效,排查了半天才发现……尴尬。

除了thenReturn,还有thenThrow(抛异常)、thenAnswer(自定义逻辑)。比如模拟网络超时:

Mockito.when(service.fetchData()).thenThrow(new IOException("Network timeout"));

这种场景在测试AOSP的网络组件时非常常见。我们不可能真的断网来测异常路径,用Stub模拟异常就行。

三、Verify:到底有没有按我说的做?

Stub关心的是“返回什么”,而Verify关心的是“做了什么”。

换句话说:你调用了一个方法,这个方法内部有没有正确调用依赖对象的某个方法?调了几次?顺序对不对?参数对不对?

给我印象最深的一次:我测试一个亮度自动调节的逻辑,Stub了光感传感器的值,但测试一直失败。后来用verify()一看,发现代码里调了两次setBrightness()——一次在构造函数里,一次在更新时。这bug本来发现不了,多亏了Verify。

// 验证:setBrightness(50) 是否被精确调用1次
Mockito.verify(mockController, Mockito.times(1)).setBrightness(50);

// 验证:从未调用过某个方法
Mockito.verify(mockController, Mockito.never()).resetBrightness();

// 验证:至少调用1次
Mockito.verify(mockController, Mockito.atLeastOnce()).applyBrightness();
请注意:Verify只对Mock对象有效。如果你把真实对象传进来,verify会直接报错。另外,Verify检查的是“交互行为”,如果某个方法没有被调用,测试就会失败。这可能会导致“过度指定”问题——你的测试绑定了内部实现细节,重构时容易误报。

四、三者关系一览

我们用一个表格来总结这三者的核心区别:

概念 核心问题 典型使用场景 注意事项
Mock对象 替换真实依赖 隔离外部组件 默认方法返回默认值
Stub 预设返回值/异常 模拟依赖的特定行为 注意参数匹配,避免Stub失效
Verify 验证交互是否发生 检查方法调用次数、顺序、参数 可能导致测试与实现过度耦合

我个人习惯:先创建Mock对象,然后用Stub设置行为,最后用Verify验证结果。这个流程几乎覆盖了90%的Mock测试写法。

五、一张图理清核心逻辑

下面这张SVG图,展示了Mock测试的总流程:

Mock测试核心流程 真实组件 Mock对象 替换 测试代码 注入 Stub预设 Verify验证 测试结果 调用时触发 运行后验证 返回预设值 通过/失败 注意流程顺序: 先创建Mock → Stub行为 → 执行测试逻辑 → 最后Verify → 得出结果

六、写一个完整的例子

光说概念不够,我拿AOSP中常见的Setting面板测试举个例子。假设我们有一个BrightnessManager,它依赖BrightnessController来实际调节亮度。我们要测试BrightnessManager.increaseBrightness()方法:

public class BrightnessManagerTest {
    private BrightnessController mockController;
    private BrightnessManager manager;

    @Before
    public void setUp() {
        // 1. 创建Mock对象
        mockController = Mockito.mock(BrightnessController.class);
        // 2. 注入到被测类
        manager = new BrightnessManager(mockController);
    }

    @Test
    public void testIncreaseBrightness_shouldCallSetBrightness() {
        // 3. Stub:模拟当前亮度为30
        Mockito.when(mockController.getBrightness()).thenReturn(30);
        // 4. 执行被测试方法
        manager.increaseBrightness(); // 内部应该调用 setBrightness(35)
        // 5. Verify:确认调用了 setBrightness(35)
        Mockito.verify(mockController).setBrightness(35);
    }

    @Test
    public void testIncreaseBrightness_atMax_shouldNotIncrease() {
        Mockito.when(mockController.getBrightness()).thenReturn(100);
        manager.increaseBrightness();
        // 不应该调用 setBrightness(已经是最大值)
        Mockito.verify(mockController, Mockito.never()).setBrightness(Mockito.anyInt());
    }
}

这个例子覆盖了三个核心概念:Mock对象(mockController)、Stub(when(...).thenReturn(...))、Verify(verify(...).setBrightness(...))。

温馨提示:写测试时,建议先写Verify再写实现代码(TDD风格)。但如果你是事后补测,先Stub再Verify更顺手。没有顺序,看个人习惯。

七、避坑指南(来自血泪教训)

  • Stub了但没被调用? 检查Mock对象是否真的被注入到测试目标里。我曾经用@InjectMocks注解时,忘了字段名要匹配,结果Stub全部落空。
  • Verify一直失败? 看看是不是参数用了eq()匹配,而实际传入的是不同的对象。对于自定义对象,记得重写equals(),或者用refEq()。
  • Mock了最终类? AOSP里有些类是final的(比如部分系统服务)。普通Mockito搞不定,需要加mock-maker-inline配置,或者改用PowerMock。我建议先看看能不能用接口代替。
  • 测试太慢? 检查是否不小心用了Mockito.spy()调用了真实构造器。纯Mock比Spy快很多,能用Mock就别用Spy。

其实,Mock基础就这么点东西。你只要记住:Mock对象做隔离,Stub定义行为,Verify验证过程。后面所有复杂的Mock技巧(ArgumentCaptor、Answer、Spy、自定义Matcher)都是锦上添花。先把这三样玩熟,AOSP组件测试就不再迷茫了。