夜雨聆风学习资料网

ARTICLE · 1092162

Mockito测试框架使用和源码解析

Mockito测试框架使用和源码解析
TUTORIAL · JAVA TEST2026.09

为测试一个方法,启动整个 SpringBoot?

一行代码请来替身演员

Mockito 从入门到源码

6 个测试用例 · 5 段源码原理 · 1 套测试哲学

MOCKITO · 单元测试实战

UNIT TESTBYTECODE

FOLLOW US · 关注我们

后台技术汇

专注后端开发 · AI · 架构 · 心法干货扫码关注,第一时间获取更新

4 Parts · Conclusion

👉 滑动查看

PART 01

揭开面纱

WHAT IS MOCKITO

PART 02

核心玩法

HANDS-ON TUTORIAL

PART 03

源码原理

UNDER THE HOOD

PART ///

写在最后

WRAP UP

Mockito 不只是测试工具,更是一种「隔离与聚焦」的测试哲学

各位技术圈的小伙伴们,大家好!在 Java 开发的世界里,单元测试绝对是保障代码质量的「护城河」。但在实际编写测试时,大家是不是经常遇到这样的痛点:被测方法依赖了复杂的数据库操作、外部 HTTP 接口,或者某个还没开发完的下游服务?为了测试一个小小的业务逻辑,我们不得不启动庞大的 Spring 容器,甚至还要造一堆假数据,不仅耗时耗力,一旦外部依赖挂了,测试也跟着报错。

这时候,咱们就需要请出单元测试界的「超级替身演员」——Mockito。它就像一位演技精湛的演员,能完美模仿任何对象的行为,让我们把注意力完全集中在核心业务逻辑上。今天这篇文章,咱们就来一场 Mockito 的深度之旅,从基础用法到源码原理,带你彻底玩转这个强大的测试框架。

01

PART

Mockito 是什么

WHAT IS MOCKITO

揭开 Mockito 的神秘面纱

简单来说,Mockito 是 Java 生态中最流行、最优雅的单元测试 Mock 框架。它的核心使命就是「隔离」。在复杂的微服务架构或分层架构中,类与类之间往往存在千丝万缕的依赖。Mockito 允许我们创建出虚拟的 Mock 对象(替身),这些对象可以模拟真实对象的方法调用、返回值甚至抛出异常,从而让我们能够在不依赖真实环境的情况下,对目标类进行纯粹的单元测试。

为什么选择 Mockito?

在 Mockito 诞生之前,EasyMock 等框架也是市场上的常客。但 EasyMock 采用的是「录制-回放」模式,代码写起来非常繁琐,且可读性较差。相比之下,Mockito 采用了更符合人类直觉的「行为驱动」风格。它的 API 设计极其优雅,比如 when(...).thenReturn(...) 这种链式调用,读起来就像在说人话。

此外,像 PowerMock 虽然功能强大,能 Mock 静态方法和私有方法,但它通过修改字节码和自定义类加载器来实现,不仅性能较差,还经常与新版 JDK 或 Spring Boot 产生兼容性问题。而 Mockito 坚持「不 Mock 静态方法」的设计哲学,反而倒逼开发者写出耦合度更低、更符合面向对象设计原则的代码。配合其丰富的核心特性(如参数匹配器、调用顺序验证、Spy 部分模拟等),Mockito 稳坐 Java 测试框架的头把交椅。

02

PART

如何使用 Mockito

HANDS-ON TUTORIAL

光说不练假把式,接下来咱们通过几个真实的测试用例,一步步拆解 Mockito 的核心玩法。

Maven 依赖配置

想要在项目中使用 Mockito,首先需要在 pom.xml 中引入相关依赖。这里推荐引入 mockito-core 和 mockito-junit-jupiter 两个包:

...xml

<dependencies>

  <!-- Mockito 核心库 -->

  <dependency>

    <groupId>org.mockito</groupId>

    <artifactId>mockito-core</artifactId>

    <version>4.11.0</version> <!-- 建议使用 4.x 或 5.x 稳定版本 -->

    <scope>test</scope>

  </dependency>

  <!-- 与 JUnit 5 集成的扩展包,支持 @Mock 等注解 -->

  <dependency>

    <groupId>org.mockito</groupId>

    <artifactId>mockito-junit-jupiter</artifactId>

    <version>4.11.0</version>

    <scope>test</scope>

  </dependency>

</dependencies>

✦ 版本建议

如果你的项目使用的是 JUnit 5,强烈建议加上 mockito-junit-jupiter,这样就能通过 @ExtendWith(MockitoExtension.class) 优雅地初始化 Mock 对象,告别繁琐的手动 mock() 调用。

核心测试用例解析

下面通过六个由浅入深的测试用例,带你掌握 Mockito 的六大核心玩法:

CASE 01

verify 验证调用行为

行为验证的利器

在单元测试中,我们不仅关心方法的返回值,有时更关心「某个方法是否被调用了」以及「调用时传了什么参数」。这就是 verify 的拿手好戏。

...java

@Test

void verifyTest() {

  // 1. 创建一个 User 类的替身演员

  User mockUser = mock(User.class);

  // 2. 调用替身的方法(就像在真实业务中调用一样)

  mockUser.setUserId("admin@coremail.cn");

  mockUser.setOrgId("a");

  // 3. 验证:替身是否真的被调用了 setUserId,且参数是 "admin@coremail.cn"

  verify(mockUser).setUserId("admin@coremail.cn");

  // 4. 验证:替身是否被调用了 setOrgId,且参数是 "b"

  // 注意:这里故意传入 "b",但实际调用的是 "a",所以这行代码会抛出断言失败异常!

  verify(mockUser).setOrgId("b");

}

核心要点:verify 是行为验证的利器。它不关心方法内部怎么执行,只关心「动作」是否发生。如果实际调用与预期不符,Mockito 会毫不留情地抛出异常,帮你精准定位 Bug。

CASE 02

stubbing 打桩(模拟返回值与异常)

给替身写好剧本

真实对象的方法是有逻辑的,但替身演员默认是「什么都不做」的。如果我们希望替身在被调用时返回特定值或抛出异常,就需要进行「打桩」(Stubbing)。

...java

@Test

void testStub() {

  User mockUser = mock(User.class);

  // 当调用 getUserId() 时,返回 "a@coremail.cn"

  when(mockUser.getUserId()).thenReturn("a@coremail.cn");

  // 当调用 getOrgId() 时,直接抛出 NoClassDefFoundError 异常

  when(mockUser.getOrgId()).thenThrow(new NoClassDefFoundError());

  // 打印结果:控制台会输出 "a@coremail.cn"

  System.out.println(mockUser.getUserId());

  // 打印结果:控制台会抛出 NoClassDefFoundError 异常

  System.out.println(mockUser.getOrgId());

}

核心要点:when...thenReturn 和 when...thenThrow 是 Mockito 最常用的打桩方式。它让替身演员拥有了「剧本」,无论调用多少次,都会严格按照剧本演出。

CASE 03

参数匹配器与行为验证进阶

不关心具体值,只关心类型与特征

在实际业务中,我们往往不关心具体的参数值,只关心参数的类型或某种特征。这时就需要用到参数匹配器。

...java

@Test

void testMatch() {

  User mockUser = mock(User.class);

  // anyString() 表示匹配任意字符串参数

  when(mockUser.setOrgIdById(anyString())).thenReturn("bb");

  // 连续调用三次,由于使用了 anyString(),都会命中上面的打桩规则

  System.out.println(mockUser.setOrgIdById(anyString())); // 输出: bb

  System.out.println(mockUser.setOrgIdById(anyString())); // 输出: bb

  System.out.println(mockUser.getOrgId());        // 未打桩,返回默认值 null

  verify(mockUser, times(1)).getOrgId();

  // 验证 setOrgIdById 被调用了 2 次,且参数精确为 "1111"

  // 注意:实际调用传的是 anyString() 匹配器,但 verify 时传了具体值 "1111"

  // 这里会触发断言失败,因为实际并没有传入 "1111"

  verify(mockUser, times(2)).setOrgIdById("1111");

}

!踩坑提示 🕳

Mockito 提供了 anyString()、anyInt()、eq() 等丰富的匹配器。一旦在 when 或 verify 中使用了参数匹配器,所有参数都必须使用匹配器,不能混用具体值和匹配器(除非用 eq("具体值") 包装)。此外,状态验证(验证返回值)和行为验证(verify)是两个不同的维度,不要混淆。

CASE 04

InOrder 调用顺序验证

验证时序关系

有些业务逻辑对方法的调用顺序有严格要求,比如「必须先校验权限,再执行扣款」。InOrder 就是用来验证这种时序关系的。

...java

@Test

void orderTest() {

  User mockUser = mock(User.class);

  // 按照特定顺序执行操作

  mockUser.setUserId("admin@coremail.cn");

  mockUser.setOrgId("a");

  // 创建 InOrder 验证器

  InOrder inOrder = inOrder(mockUser);

  // 验证:setOrgId 必须在 setUserId 之前被调用

  // 注意:实际代码中 setUserId 先执行,所以这里会断言失败!

  inOrder.verify(mockUser).setOrgId("a");

  inOrder.verify(mockUser, times(1)).setUserId("admin@coremail.cn");

}

核心要点:InOrder 可以验证单个对象的调用顺序,也可以传入多个 Mock 对象来验证跨对象的交互顺序。它让时序逻辑的测试变得简单直观。

CASE 05

连续打桩(Consecutive Stubbing)

模拟状态变化的绝佳工具

在某些场景下,同一个方法在不同时刻需要返回不同的值,比如模拟网络重试、分页查询或状态流转。

...java

@Test

void consecutiveStubTest() {

  User mockUser = mock(User.class);

  // 链式打桩:按顺序依次返回不同结果

  when(mockUser.getUserId())

      .thenReturn("admin@coremail.cn") // 第 1 次调用返回这个

      .thenReturn("1")         // 第 2 次调用返回这个

      .thenReturn("2")         // 第 3 次调用返回这个

      .thenThrow(new RuntimeException()); // 第 4 次调用抛出异常

  System.out.println(mockUser.getUserId()); // 输出: admin@coremail.cn

  System.out.println(mockUser.getUserId()); // 输出: 1

  System.out.println(mockUser.getUserId()); // 输出: 2

  System.out.println(mockUser.getUserId()); // 抛出 RuntimeException

}

核心要点:链式 thenReturn 是模拟状态变化的绝佳工具。当所有预设的返回值耗尽后,最后一次的行为会一直生效(本例中最后一次是抛异常,后续调用都会抛异常)。

CASE 06

spy 部分模拟(真实与虚拟的结合)

保留真实逻辑,只修改一两个方法

有时候我们不想完全伪造一个对象,而是希望保留它的真实逻辑,仅仅修改其中一两个方法。这时 spy(间谍) 就派上用场了。

...java

@Test

void spyTest() {

  // 创建一个真实的 User 对象

  User mockUser = new User("admin@coremail.cn", "a");

  // 给它套上「间谍」外壳

  User spy = spy(mockUser);

  // 仅对 getUserId 方法进行打桩,其他方法保持真实逻辑

  when(spy.getUserId()).thenReturn("mango");

  System.out.println(spy.getOrgId());   // 输出: a(调用了真实方法)

  System.out.println(spy.getUserId());  // 输出: mango(调用了打桩方法)

  // 安全打桩:使用 doReturn...when 语法

  // 为什么用 doReturn?因为 spy 调用真实方法可能会触发副作用或空指针

  doReturn("b").when(spy).setOrgIdById("1");

  System.out.println(spy.setOrgIdById("1")); // 输出: b

}

!踩坑提示 🕳

spy 与 mock 的最大区别在于,mock 默认所有方法都不执行真实逻辑,而 spy 默认执行真实逻辑。强烈建议:对 spy 对象打桩时,尽量使用 doReturn...when 而不是 when...thenReturn,因为后者会先执行一次真实方法,容易引发意外问题。

03

PART

Mockito 的源码原理

UNDER THE HOOD

用熟了 Mockito,很多小伙伴可能会好奇:它到底是怎么做到「凭空捏造」一个对象,还能精准记录每次调用的?咱们来扒一扒它的底层源码。

核心基石:Java 代理机制

Mockito 并不是什么黑魔法,它的底层完全依赖于 Java 的动态代理技术。在早期版本中,Mockito 使用的是 JDK 自带的 DynamicProxy,但它只能代理接口。为了支持代理普通类,Mockito 后来引入了强大的字节码操作库 ByteBuddy(早期是 CGLIB)。当你调用 mock(User.class) 时,ByteBuddy 会在内存中动态生成一个 User 的子类,这个子类重写了所有非 final 方法,并在方法内部插入了拦截逻辑。

mock() 方法底层做了什么?

创建代理对象

ByteBuddy 生成子类并实例化

→

绑定 MockHandler

替身演员的「大脑」

→

记录交互

ThreadLocal 暂存每次调用

当你调用 mock() 时,Mockito 内部的三个关键步骤

when() 方法的实现原理

第一步:执行真实调用。mock.method() 会先被执行一次,这次调用会被 MockHandler 拦截,并将期望的行为(如 thenReturn("a"))暂存到当前线程的 ThreadLocal 中。

第二步:绑定行为。when() 方法会从 ThreadLocal 中取出刚才暂存的期望行为,并将其与当前的方法签名绑定起来,存入 Mock 对象的内部映射表中。这就是为什么 when 语法看起来那么自然,但也解释了为什么它对 void 方法或 spy 对象不太友好(因为会真实执行一次)。

verify() 的实现原理

verify 的本质是一个「查询 + 匹配」的过程。当你调用 verify(mock).method() 时,Mockito 会进入「验证模式」,从 Mock 对象内部维护的 InvocationContainer(交互记录容器)中遍历之前所有的调用记录;使用参数匹配器(如 anyString()、eq())对记录的参数进行逐一比对。如果找到匹配的记录,则验证通过;找不到,或者调用次数不符合 times(n) 的预期,就会抛出详细的 WantedButNotInvoked 异常,并打印出所有实际的调用记录,帮你快速排查问题。

插件机制与扩展点

Mockito 的设计非常开放,它提供了丰富的 SPI(Service Provider Interface)扩展点。比如,你可以通过自定义 MockMaker 来改变代理的生成方式(如支持 Mock 静态方法的 mockito-inline);可以通过 Answer 接口自定义方法拦截时的响应逻辑;还可以通过 MockitoListener 监听整个测试生命周期。这种插件化的架构,让 Mockito 在保持核心轻量的同时,拥有了无限的扩展可能。

///

LAST

写在最后

WRAP UP

从基础的 verify 和 when,到进阶的 InOrder 和 spy,再到深入骨髓的 ByteBuddy 代理原理,咱们今天把 Mockito 从外到内扒了个底朝天。Mockito 不仅仅是一个测试工具,它更是一种「隔离与聚焦」的测试哲学。它教会我们在编写代码时,时刻思考依赖关系,追求低耦合、高内聚的设计。

纸上得来终觉浅,绝知此事要躬行。希望各位小伙伴看完这篇文章后,不要只停留在「看懂了」的层面。赶紧打开你的 IDE,找一个现有的项目,试着用今天学到的知识重构几个单元测试,或者为新的业务逻辑编写一套完美的 Mock 测试。当你在绿色的测试通过提示中看到自己的代码稳如泰山时,那种成就感,绝对是写业务代码无法比拟的!

愿大家的代码永远没有 Bug

单元测试永远一路绿灯!

我是 后台技术汇,旨在分享原创技术知识,提升核心竞争力。

如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见

既然看到这里了,如果觉得有用,随手点个赞、在看、转发三连吧。

点赞
在看
转发

THANKS FOR READING

相关学习资料