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

下面这张图从创建、行为、适用场景三个维度做了对比,看一眼应该就能记住。

mock vs spy 核心区别 mock 完全伪造对象 所有方法返回默认值 可通过 when() 自定义行为 适合:系统组件、不可控依赖 spy 部分真实对象 未打桩的方法执行真实逻辑 可用 when() 覆盖个别方法 适合:业务Pojo、遗留代码 vs 如何选择?→ 看你的需求 需要完全隔离? 选 mock 只改个别方法? 选 spy 真实对象有副作用? 只能 mock 💡 总结 mock 全假,spy 半真半假。 优先用 mock,只有需要保留大部分真实行为时才用 spy。 Mockito 4.x | 适用于 Android 权限 Mock 测试

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,说明你的被测类设计可能不够松耦合,考虑重构。