ARTICLE · 1092162
Mockito测试框架使用和源码解析
为测试一个方法,启动整个 SpringBoot?
一行代码请来替身演员
Mockito 从入门到源码
6 个测试用例 · 5 段源码原理 · 1 套测试哲学
MOCKITO · 单元测试实战
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 两个包:
<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 的六大核心玩法:
verify 验证调用行为
行为验证的利器
在单元测试中,我们不仅关心方法的返回值,有时更关心「某个方法是否被调用了」以及「调用时传了什么参数」。这就是 verify 的拿手好戏。
@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。
stubbing 打桩(模拟返回值与异常)
给替身写好剧本
真实对象的方法是有逻辑的,但替身演员默认是「什么都不做」的。如果我们希望替身在被调用时返回特定值或抛出异常,就需要进行「打桩」(Stubbing)。
@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 最常用的打桩方式。它让替身演员拥有了「剧本」,无论调用多少次,都会严格按照剧本演出。
参数匹配器与行为验证进阶
不关心具体值,只关心类型与特征
在实际业务中,我们往往不关心具体的参数值,只关心参数的类型或某种特征。这时就需要用到参数匹配器。
@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)是两个不同的维度,不要混淆。
InOrder 调用顺序验证
验证时序关系
有些业务逻辑对方法的调用顺序有严格要求,比如「必须先校验权限,再执行扣款」。InOrder 就是用来验证这种时序关系的。
@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 对象来验证跨对象的交互顺序。它让时序逻辑的测试变得简单直观。
连续打桩(Consecutive Stubbing)
模拟状态变化的绝佳工具
在某些场景下,同一个方法在不同时刻需要返回不同的值,比如模拟网络重试、分页查询或状态流转。
@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 是模拟状态变化的绝佳工具。当所有预设的返回值耗尽后,最后一次的行为会一直生效(本例中最后一次是抛异常,后续调用都会抛异常)。
spy 部分模拟(真实与虚拟的结合)
保留真实逻辑,只修改一两个方法
有时候我们不想完全伪造一个对象,而是希望保留它的真实逻辑,仅仅修改其中一两个方法。这时 spy(间谍) 就派上用场了。
@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