4. Mock 概念与框架
说到 Mock 测试,我想先聊聊一个很常见的场景——你写完了一个模块,想跑单元测试,结果发现它依赖了好几个外部服务:数据库、REST API、甚至消息队列。没有这些服务的运行环境,测试就跑不起来。你想想看,这多烦人。
我记得在我刚转 Java 那会儿,就吃过这个亏。有一次我负责一个支付结算模块,单元测试里还得启动一个本地的 MySQL 实例。代码改了之后跑一遍测试,先得等 10 秒让数据库启动,然后清表、插数据、跑用例、再清表……一天下来,大部分时间花在了「等」上,而不是写代码上。
后来我遇到了一位老同事,他告诉我:「你试试 Mock。」 这个词当时听起来有点玄学。说白了,Mock 就是「假装」。假装那个外部服务是真的,假装它返回了你期望的数据。这样,你的测试就可以脱离真实的数据库、网络、文件系统,只测试你代码本身的逻辑。
所以这一章,我们就来彻底搞清楚:Mock 到底解决了什么问题?主流的框架有哪些?以及,在多模块的 Gradle 项目里,我应该选哪个?
4.1 Mock 测试的意义
Mock 的核心价值,在我看来就三条:
- 解耦测试依赖 — 你想测试模块 A,它调用了模块 B 的接口。模块 B 还写着呢,或者它依赖数据库。有了 Mock,你可以让 A 的测试独立跑,不需要 B 真的部署。
- 提升测试速度 — 我在项目里实测过,一个依赖 Spring 容器的集成测试可能需要 10 秒;而一个 Mock 掉外部依赖的纯 JUnit 测试,50 毫秒就完成了。差别有多大?你现在就能感受到。
- 模拟边界场景 — 比如网络超时、数据库连接池耗尽、第三方返回异常 HTTP 400。这些场景在真实环境很难复现,但在 Mock 里只需要一行代码就能模拟出来。
当然,Mock 也不是万能的。如果你完全 Mock 掉一切,那测试就失去了对真实集成的校验能力。我个人习惯是:「单元测试尽量 Mock,集成测试尽量真实」。把握好这个度,项目质量才能真正有保障。
4.2 常用 Mock 框架概览
Java 生态中最常用的 Mock 框架,我接触过的有三个:Mockito、MockK、以及 EasyMock。我个人的经验是:在纯 Java 项目里,Mockito 几乎是事实标准;在 Kotlin 项目里,MockK 是更优的选择。
下面这张表你直接拿来用,后面选框架的时候参考就行:
| 框架 | 版本(推荐) | 语言 | 核心特性 | 适用场景 |
|---|---|---|---|---|
| Mockito | 5.x | Java | 灵活、API 简洁、与 JUnit 5 集成好 | 纯 Java 项目、Spring Boot 项目 |
| MockK | 1.13+ | Kotlin | 支持协程、可空类型、lambda 隔离 | Kotlin 多模块项目、协程测试 |
| EasyMock | 5.x | Java | 偏传统的录制-验证模式 | 老旧项目升级场景 |
4.3 Mockito 实战要点
Mockito 是我用得最多的框架。它的语法非常直接,你几乎不用学什么特别的 DSL。来看一个最简单的例子:
import static org.mockito.Mockito.*;
public class OrderServiceTest {
@Test
void testGetOrder() {
// 1. 创建 Mock 对象
PaymentGateway gateway = mock(PaymentGateway.class);
// 2. 定义行为
when(gateway.charge(anyString(), eq(100.0)))
.thenReturn(true);
// 3. 注入并执行业务逻辑
OrderService service = new OrderService(gateway);
boolean result = service.processOrder("order-123");
// 4. 验证交互
verify(gateway).charge("order-123", 100.0);
assertTrue(result);
}
}
这里要注意几个点:
mock()方法会生成一个空壳对象,所有方法默认返回 null 或零值。when().thenReturn()是最常见的「打桩」方式。我曾踩过一个坑:如果用了any()匹配器,所有的参数匹配都必须换成匹配器,不能混用真实值。否则会抛出InvalidUseOfMatchersException。verify()用来检查被测方法是否真的调用了某个依赖——我第一次用 Mockito 时没注意这步,后来发现某个模块外部接口根本没被正确调用,而测试通过了。所以我现在习惯在每个关键路径上加 verify。
mockConstruction() API(Mockito 5+ 支持)。
4.4 MockK:Kotlin 项目的首选
如果你在写 Kotlin 多模块项目,我个人强烈建议你用 MockK 替代 Mockito。为什么呢?因为 MockK 对 Kotlin 的协程、可空类型、lambda 做了更优雅的支持。举个我踩过的坑当例子:
// 这是 MockK 的写法,简洁得让人感动
class UserServiceTest {
@Test
fun `test fetch user`() = runTest {
val repo = mockk<UserRepository>()
every { repo.findUser("alice") } returns User("alice", 30)
val service = UserService(repo)
val user = service.fetchUser("alice")
assertEquals("alice", user.name)
verify { repo.findUser("alice") }
}
}
这里有几句关键语法:
mockk()创建 Mock 对象。every { ... } returns ...等价于 Mockito 的when().thenReturn(),但更加类型安全。- 对于 suspend 函数,MockK 自带支持,不需要额外配置。我见过一个团队用 Mockito 测协程,不得不在测试里加
blockingGet()或runBlocking,非常别扭。 - 另外,MockK 的
verifySequence可以检验多个调用的顺序,这在测试复杂业务流程时特别好用。
4.5 如何选择 Mock 框架?
你可能会问:这么多框架,我到底该选哪个?我给几条实际建议:
- 如果项目 90% 以上是 Java,直接用 Mockito 5.x,社区大、资料多、踩坑少。
- 如果项目包含 Kotlin 模块,尤其是用到协程、sealed class 或者 data class,那就在 Kotlin 模块里用 MockK。别硬撑。
- 如果项目是遗留的,已经在用 EasyMock,可以维持不动。但从我个人的升级经验看,没必要主动从 EasyMock 迁移——除非你遇到严重的性能问题或缺少协程支持。
- 如果团队对测试风格有统一要求,建议使用 Mockito + MockK 组合。用 Gradle 的
testImplementation分别导入即可:
// 在 Java 模块的 build.gradle 中
dependencies {
testImplementation "org.mockito:mockito-core:5.13.0"
testImplementation "org.mockito:mockito-junit-jupiter:5.13.0"
}
// 在 Kotlin 模块的 build.gradle.kts 中
dependencies {
testImplementation("io.mockk:mockk:1.13.12")
}
4.6 核心 Mock 技术术语
我知道刚开始接触 Mock 时,这些术语容易混。我整理了一个快速对照表,你开发时当个速查手册用:
| 术语 | 含义 | Mockito 对应 | MockK 对应 |
|---|---|---|---|
| Mock 对象 | 替身对象,不执行真正逻辑 | mock() |
mockk() |
| Stub(打桩) | 预设方法返回值 | when().thenReturn() |
every { } returns |
| Spy | 保留真实方法,局部打桩 | spy() |
spyk() |
| Verify | 验证某方法是否被调用 | verify() |
verify { } |
| Argument Matcher | 灵活匹配方法参数 | any(), eq() |
any(), eq() |
@InjectMocks(Mockito)或 @RelaxedMockK 注解,它们能帮你自动注入 Mock 对象到被测类中。但注意,不要过度依赖自动注入——它可能会让测试的依赖关系变得不透明。我一般只在简单场景下用。
4.7 避坑指南:从实战出发
我记得有一次,我在一个多模块项目里用了 Mockito 的 mockStatic() 来 Mock 一个静态工具类。结果测试通过了,但后续版本升级时,那个静态方法换了签名,我们的测试没有及时发现。为什么?因为 Mock 掉了静态方法,真实调用链路被切断了。从那以后,我对 Mock 静态方法就特别小心。
这里给出几个我踩过的坑:
- 不要 Mock 掉你自己的私有方法。 如果需要测试私有方法逻辑,重构它——提取成包级可见或 protected 方法,或者干脆拆分成小方法。
- 不要在集成测试里过度 Mock。 集成测试的目的就是验证真实交互。如果你把数据库和外部 API 都 Mock 了,那这个测试的价值就大打折扣。
- 版本一致性。 如果用了 Junit 5 @ExtendWith(MockitoExtension.class),请确保 mockito-junit-jupiter 与 mockito-core 版本一致。我见过一个团队因为版本不匹配,测试一直报 IllegalStateException,排查了一下午。
- 在 Gradle 多模块中保持依赖版本统一。 建议在根项目的
build.gradle中使用dependencyConstraints或platform统一管理 Mock 框架版本,避免子模块各自声明不同版本。