Mock测试核心概念:从理论到实战

聊Mock测试,绕不开三个基础问题:Mock和Stub到底有啥区别?录制回放模式是怎么回事?市面上框架那么多,怎么选?这一节我结合自己踩过的坑,把这些问题一次性理清楚。

一、Mock对象与Stub:别再傻傻分不清

很多人把Mock和Stub混着用,其实它们的职责完全不同。Stub是替身演员,负责给答案;Mock是考官,负责检查你怎么演的。

举个例子:你写了个订单服务,需要调用支付网关。用Stub的话,你只要它返回“支付成功”就行;用Mock的话,你不仅要结果,还得验证“调用了支付接口一次、参数正确”。

我刚开始做单元测试时,以为Mock就是Stub,结果测试通过了但代码里根本没调用支付接口——因为Stub只是返回数据,不记录调用次数。后来被同事吐槽了一顿,才彻底搞明白。

测试替身家族:Mock vs Stub vs Fake vs Dummy 测试替身 (Test Double) Dummy(占位) Stub(存根) Fake(伪造) Mock(模拟) Stub(存根) • 返回预设数据,不记录调用 • 用于“被测对象需要外部数据” • 不验证行为,只提供结果 Mock(模拟) • 验证交互次数、参数、顺序 • 用于“确保依赖被正确调用” • 预设行为 + 事后断言 Spy(间谍):部分模拟,保留真实行为 扩展

核心区别一句话:Stub回答“是什么”,Mock确认“怎么调用”。如果你的测试只关心返回值,用Stub;如果要验证调用协议,用Mock。

特性Stub(存根)Mock(模拟)
核心目的提供预设数据验证交互行为
是否记录调用否是(次数/参数/顺序)
是否需要断言不需要需要(verify步骤)
典型场景DAO层返回固定查询结果检查支付接口被调用了1次
框架示例Mockito.when().thenReturn()Mockito.verify()

二、录制回放模式:当Mock遇上"黑匣子"

录制回放模式,说白了就是:先把真实服务的请求和响应录下来,然后在测试环境里“演”给你看。我第一次接触这个模式是在做第三方支付集成测试时——对方没有沙箱环境,只能靠录制一整天的手动操作数据来跑回归。

它的工作流程其实很直观:

  1. 录制阶段:代理拦截真实服务调用,把请求参数、响应结果、耗时等信息存起来(通常存成文件或数据库)。
  2. 存储阶段:生成匹配规则和响应模板,可以是简单的键值对,也可以带条件匹配。
  3. 回放阶段:测试运行时,代理根据请求特征找到匹配的录制记录,直接返回存储的响应。
  4. 验证阶段:检查实际调用的请求是否与录制的匹配,也可以验证次数和顺序。

踩坑提醒:我曾经在回放时遇到请求参数带时间戳、随机数等情况,导致怎么都匹配不上。解决办法是配置忽略字段或使用正则匹配。另外,录制数据要定期更新,否则旧的响应可能无法反映当前服务的正确行为。

为什么需要录制回放?场景很明确:

  • 外部依赖不稳定(比如测试环境连不上第三方API)
  • 需要重现历史问题(录制错误场景)
  • 团队并行开发,不想互相依赖
  • 压测时模拟高并发响应

三、常用Mock框架:选型如选武器

市面上的Mock框架不少,我按使用场景把它们分成三类,给你参考。

框架类型特点我的推荐场景
Mockito普通Mock/Stub语法简洁、社区活跃、支持注解99%的Java单元测试场景
EasyMock普通Mock/Stub更严格,需要显式声明预期行为老项目迁移、需要严格验证时
JMockit高阶Mock能mock静态方法/构造器/私有方法遗留代码测试、无法重构的Class
PowerMock补丁类Mock扩展Mockito/EasyMock,突破限制终极手段,尽量少用(性能差)
WireMockHTTP Mock回放支持录制回放、独立运行、Stub映射微服务集成测试、第三方API模拟
MockServerHTTP 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\"}")));

框架核心概念速查表

概念MockitoWireMock说明
创建Mock对象mock(Class)WireMockServer生成代理实例
预设行为when().thenReturn()stubFor()定义Stub返回
验证交互verify()count = findUnmatchedRequests()检查调用是否发生
录制回放不支持原生录制snapshot() + saveMappings()WireMock支持将真实请求存为Stub

嗯,到这里你大概心里有数了:Stub和Mock的区别、录制回放的工作原理、以及几个主流框架的定位。别让这些概念成为写测试的绊脚石,多用几次自然就顺手了。