Mock基础:Mock对象、Stub、Verify等概念
说实话,很多人刚接触Mock时,容易被各种术语绕晕。Mock、Stub、Spy、Verify……这些到底有啥区别?我当年第一次写单元测试,对着Mockito文档看了半天,脑子里就一句话——这玩意儿到底怎么用?
后来踩了不少坑,才总结出一个经验:你不需要一次性记住所有概念,只需要搞懂三样东西——Mock对象、Stub行为、Verify验证。其他所有高级玩法,都是这三样组合出来的。
一、Mock对象到底是什么?
说白了,Mock对象就是一个“替身演员”。我们在测试中不想依赖真实组件(比如网络请求、数据库、硬件接口),就用Mock对象替换掉它们。
为什么要这么做?举个例子:你在做AOSP的SystemUI测试,想验证“点击某个按钮后,亮度条是否被正确更新”。但真实环境下,亮度调节依赖硬件驱动。如果你用真实设备,每次测试都得等硬件响应,慢得要命,而且可能产生副作用(比如屏幕真的变暗了)。
这时候,Mock对象就派上用场了。它模拟了真实对象的行为,但不需要实际硬件参与。测试跑得飞快,而且完全可控。
在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);
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();
四、三者关系一览
我们用一个表格来总结这三者的核心区别:
| 概念 | 核心问题 | 典型使用场景 | 注意事项 |
|---|---|---|---|
| Mock对象 | 替换真实依赖 | 隔离外部组件 | 默认方法返回默认值 |
| Stub | 预设返回值/异常 | 模拟依赖的特定行为 | 注意参数匹配,避免Stub失效 |
| Verify | 验证交互是否发生 | 检查方法调用次数、顺序、参数 | 可能导致测试与实现过度耦合 |
我个人习惯:先创建Mock对象,然后用Stub设置行为,最后用Verify验证结果。这个流程几乎覆盖了90%的Mock测试写法。
五、一张图理清核心逻辑
下面这张SVG图,展示了Mock测试的总流程:
六、写一个完整的例子
光说概念不够,我拿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(...))。
七、避坑指南(来自血泪教训)
- 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组件测试就不再迷茫了。