4. Mock框架入门:Mockito基本用法、mock与spy的区别
说实话,刚接触单元测试那会儿,我也踩过不少坑。尤其是写Android权限相关的测试时,被测代码里动不动就要检查checkSelfPermission,或者调requestPermissions。如果没有Mock框架,你得把整个系统服务都搭出来——那工作量,啧啧,想想就头疼。
Mockito就是来救场的。它让我们能轻松"伪造"依赖对象,把测试聚焦在业务逻辑上。今天咱们就来聊聊Mockito最核心的两个用法:mock和spy,以及它们到底有啥区别。
4.1 Mockito基本用法
先看一个最典型的场景:我要测试一个PermissionChecker类,它依赖Context去判断权限是否已授予。在测试里,我可不想搭一个真机或模拟器,直接Mock一个Context完事。
// 引入静态方法,让代码更简洁
import static org.mockito.Mockito.*;
public class PermissionCheckerTest {
@Test
public void testCheckPermission_granted() {
// 1. mock 一个 Context
Context mockContext = mock(Context.class);
// 2. 设定行为:当调用 checkSelfPermission 且参数为 CAMERA 时,返回 PackageManager.PERMISSION_GRANTED
when(mockContext.checkSelfPermission(Manifest.permission.CAMERA))
.thenReturn(PackageManager.PERMISSION_GRANTED);
// 3. 执行被测类
PermissionChecker checker = new PermissionChecker(mockContext);
boolean result = checker.hasCameraPermission();
// 4. 验证结果
assertTrue(result);
}
}
嗯,这里有几个关键点:
mock():创建一个空壳对象,所有方法都返回默认值(int返回0,boolean返回false,对象返回null)。when().thenReturn():定义"当调用某个方法时,应该返回什么值"。verify():验证某个方法是否被调用了(后面章节细讲)。
我的习惯:写测试时,我总先用 mock() 创建依赖,然后 when().thenReturn() 把需要的行为钉死。这样测试很稳固——被Mock的对象不会因为外部变化而影响结果。
4.2 mock与spy的区别
很多同学刚学Mockito时,会问:spy是啥?和mock有什么不同?
我用一个例子讲明白。假设我们有一个UserManager类,里面有个方法getUserName(),它内部调用了另一个方法fetchFromRemote()。
public class UserManager {
public String getUserName() {
String name = fetchFromRemote();
return name == null ? "default" : name;
}
public String fetchFromRemote() {
// 真实场景会发网络请求
return null;
}
}
现在我们想测试getUserName(),但不想真的发网络请求。用mock的话,整个对象的方法都会被替换,你完全控制行为。用spy的话,它保留真实对象,只让你"部分mock"——也就是说,没被when()打过的方法,会走真实逻辑。
| 对比项 | mock | spy |
|---|---|---|
| 创建方式 | mock(Class) |
spy(Class) 或 spy(Object) |
| 默认行为 | 所有方法返回默认值(0/null/false) | 所有方法执行真实逻辑 |
| 是否可部分mock | 可以,但整个对象都是假的 | 可以,仅对打桩的方法覆盖真实逻辑 |
| 什么时候用 | 完全替换依赖 | 保留大部分真实行为,只修改一两处 |
| 调用真实方法 | 除非用 doCallRealMethod() |
默认调用真实方法,除非被when()覆盖 |
我曾经踩过一个坑:对一个真正的List对象用了mock(),然后调用list.get(0)时,它返回null,而不是抛出IndexOutOfBoundsException。这跟真实List的行为完全不同。后来我改用spy(new ArrayList<>()),只覆盖我关心的add()方法,别的保持真实,测试才符合预期。
4.3 实战场景:选mock还是spy?
来看看我实际写权限测试时的选择:
- mock:当被测类依赖
Context、PackageManager、Activity这类系统组件时,我几乎只用mock。因为这些组件很难实例化,而且我们只关心它们对外暴露的方法,不需要真实行为。 - spy:当被测类是业务Pojo或已有的工具类(比如自定义的
PermissionHelper),我想保留大部分逻辑,只拦截其中一两个方法调用时,spy就是首选。例如,spy(new PermissionHelper()),然后只对showPermissionDialog()打桩,避免弹出真对话框。
核心原则:能mock就不要spy。因为spy是真实对象,它的构造方法、成员变量初始化都可能带来副作用。只有当你真正需要"部分真实"时,才考虑spy。
4.4 一张图看懂mock vs spy
下面这张图从创建、行为、适用场景三个维度做了对比,看一眼应该就能记住。
4.5 代码对比:mock vs spy 怎么写
来个实际对比,你感受更明显。
// mock 方式
Context mockContext = mock(Context.class);
when(mockContext.checkSelfPermission(anyString())).thenReturn(PERMISSION_GRANTED);
// spy 方式
PermissionHelper realHelper = new PermissionHelper();
PermissionHelper spyHelper = spy(realHelper);
// 只拦截 showDialog 方法,其他方法(如 checkPermission)仍走真实逻辑
doNothing().when(spyHelper).showDialog(anyString());
// 注意:用 spy 时,when().thenReturn() 会先执行真实方法再打桩,容易出错
// 推荐用 doReturn().when() 或 doNothing().when() 避免调用真实方法
doReturn(true).when(spyHelper).checkPermission(anyString());
一个常见陷阱:对 spy 对象使用 when(spyObject.someMethod()).thenReturn(xxx) 时,someMethod() 会被真实调用一次!如果你不想触发真实逻辑,永远用 doReturn() 或 doNothing() 形式。我以前就因为这点踩过坑,测了一个小时的 bug 最后发现是顺序问题。
4.6 最佳实践小结
- 小步快跑:先写一个简单的 mock 测试,跑通后再加 spy。
- 命名清晰:变量名里带
Mock或Spy后缀,比如mockContext、spyHelper,别人看代码一目了然。 - 善用 @Mock 和 @InjectMocks:配合 JUnit 的 Rule 可以减少样板代码(后面章节会讲)。
- 避免过度 mock:如果测试里有七八个 mock,说明你的被测类设计可能不够松耦合,考虑重构。