Mock测试核心概念:从理论到实战
聊Mock测试,绕不开三个基础问题:Mock和Stub到底有啥区别?录制回放模式是怎么回事?市面上框架那么多,怎么选?这一节我结合自己踩过的坑,把这些问题一次性理清楚。
一、Mock对象与Stub:别再傻傻分不清
很多人把Mock和Stub混着用,其实它们的职责完全不同。Stub是替身演员,负责给答案;Mock是考官,负责检查你怎么演的。
举个例子:你写了个订单服务,需要调用支付网关。用Stub的话,你只要它返回“支付成功”就行;用Mock的话,你不仅要结果,还得验证“调用了支付接口一次、参数正确”。
我刚开始做单元测试时,以为Mock就是Stub,结果测试通过了但代码里根本没调用支付接口——因为Stub只是返回数据,不记录调用次数。后来被同事吐槽了一顿,才彻底搞明白。
核心区别一句话:Stub回答“是什么”,Mock确认“怎么调用”。如果你的测试只关心返回值,用Stub;如果要验证调用协议,用Mock。
| 特性 | Stub(存根) | Mock(模拟) |
|---|---|---|
| 核心目的 | 提供预设数据 | 验证交互行为 |
| 是否记录调用 | 否 | 是(次数/参数/顺序) |
| 是否需要断言 | 不需要 | 需要(verify步骤) |
| 典型场景 | DAO层返回固定查询结果 | 检查支付接口被调用了1次 |
| 框架示例 | Mockito.when().thenReturn() | Mockito.verify() |
二、录制回放模式:当Mock遇上"黑匣子"
录制回放模式,说白了就是:先把真实服务的请求和响应录下来,然后在测试环境里“演”给你看。我第一次接触这个模式是在做第三方支付集成测试时——对方没有沙箱环境,只能靠录制一整天的手动操作数据来跑回归。
它的工作流程其实很直观:
- 录制阶段:代理拦截真实服务调用,把请求参数、响应结果、耗时等信息存起来(通常存成文件或数据库)。
- 存储阶段:生成匹配规则和响应模板,可以是简单的键值对,也可以带条件匹配。
- 回放阶段:测试运行时,代理根据请求特征找到匹配的录制记录,直接返回存储的响应。
- 验证阶段:检查实际调用的请求是否与录制的匹配,也可以验证次数和顺序。
踩坑提醒:我曾经在回放时遇到请求参数带时间戳、随机数等情况,导致怎么都匹配不上。解决办法是配置忽略字段或使用正则匹配。另外,录制数据要定期更新,否则旧的响应可能无法反映当前服务的正确行为。
为什么需要录制回放?场景很明确:
- 外部依赖不稳定(比如测试环境连不上第三方API)
- 需要重现历史问题(录制错误场景)
- 团队并行开发,不想互相依赖
- 压测时模拟高并发响应
三、常用Mock框架:选型如选武器
市面上的Mock框架不少,我按使用场景把它们分成三类,给你参考。
| 框架 | 类型 | 特点 | 我的推荐场景 |
|---|---|---|---|
| Mockito | 普通Mock/Stub | 语法简洁、社区活跃、支持注解 | 99%的Java单元测试场景 |
| EasyMock | 普通Mock/Stub | 更严格,需要显式声明预期行为 | 老项目迁移、需要严格验证时 |
| JMockit | 高阶Mock | 能mock静态方法/构造器/私有方法 | 遗留代码测试、无法重构的Class |
| PowerMock | 补丁类Mock | 扩展Mockito/EasyMock,突破限制 | 终极手段,尽量少用(性能差) |
| WireMock | HTTP Mock回放 | 支持录制回放、独立运行、Stub映射 | 微服务集成测试、第三方API模拟 |
| MockServer | HTTP Mock回放 | 支持Forward、随时修改行为 | 需要动态调整Mock规则的场景 |
我个人最常用的是Mockito + WireMock组合。单元测试用Mockito,集成测试用WireMock。至于PowerMock,除非迫不得已(比如要mock System.currentTimeMillis()),否则我会优先用依赖注入重构代码——毕竟,好的设计不应该让测试这么痛苦。
一点经验:选框架时别只盯着功能,要看生态。Mockito的文档和StackOverflow回答数量吊打其他框架,团队新手上手快。如果项目已经用了Spring Boot,Spring直内置了Mockito和MockBean,你甚至不用额外引入版本。
快速上手示例
用Mockito写个Stub和Mock,感受一下差异:
// Stub:只关心返回值
OrderService orderService = mock(OrderService.class);
when(orderService.getOrder("123")).thenReturn(new Order("123", "paid"));
// Mock:验证行为
PaymentGateway gateway = mock(PaymentGateway.class);
orderService.pay("123", 100.0);
verify(gateway).charge("123", 100.0); // 检查charge方法被调用且参数正确
录制回放用WireMock更直观:
// 录制阶段(启动WireMock并配置录制)
WireMockServer wireMockServer = new WireMockServer(8089);
wireMockServer.start();
// 然后让被测代码通过代理把请求发到WireMock,它会自动录制
// 回放阶段(直接加载录制的Stub映射)
WireMock.stubFor(get(urlEqualTo("/api/user"))
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"name\":\"Alice\"}")));
框架核心概念速查表
| 概念 | Mockito | WireMock | 说明 |
|---|---|---|---|
| 创建Mock对象 | mock(Class) | WireMockServer | 生成代理实例 |
| 预设行为 | when().thenReturn() | stubFor() | 定义Stub返回 |
| 验证交互 | verify() | count = findUnmatchedRequests() | 检查调用是否发生 |
| 录制回放 | 不支持原生录制 | snapshot() + saveMappings() | WireMock支持将真实请求存为Stub |
嗯,到这里你大概心里有数了:Stub和Mock的区别、录制回放的工作原理、以及几个主流框架的定位。别让这些概念成为写测试的绊脚石,多用几次自然就顺手了。