Mock回放测试的核心原理
讲Mock回放之前,我想先问个问题:你们有没有遇到过这种场景——Crash复现了,但线上环境已经被重启,日志也刷没了。你盯着一个堆栈堆栈里缺了几帧,根本不知道用户操作到哪一步才炸的?
我遇到过,还不止一次。早期我做客户端稳定性测试时,最头疼的就是「能复现但难定位」。后来接触到Mock回放测试,思路一下子打开了。说白了,它的本质就一句话:
把真实请求录下来,在隔离环境中「原样重放」,观察Crash是否复现。
为什么叫「Mock回放」?
这个名字有点绕。我拆开讲:
- Mock —— 伪造外部依赖。比如数据库、缓存、第三方API,全用模拟对象替代。你想想看,你在重放时还想真的调用生产环境?风险太大。
- 回放 —— 把录制好的请求/响应序列按时间顺序重新发送给被测系统。
所以Mock回放 = 用模拟替代 + 按原序重放。它和普通压力测试的最大区别在于:回放的数据来自真实用户,不是合成脚本。这一点非常关键。
核心流程:录、存、放、比
我个人习惯把Mock回放分成四个阶段。画了张图,你们先看整体脉络:
下面我逐一展开每个阶段。注意,这里有不少我踩过的坑,我会直接说出来。
阶段一:录制——不是所有流量都值得录
录制的核心是无侵入地拦截请求。我常用的是AOP(面向切面)或代理模式,在客户端和服务端之间插一个拦截层。
// 伪代码示例:AOP拦截器
@Around("execution(* com.example.service.*.*(..))")
public Object recordAndProceed(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
String method = pjp.getSignature().getName();
// 录制请求
recorder.save(new RequestRecord(method, args));
Object result = pjp.proceed();
// 录制响应
recorder.updateResponse(result);
return result;
}
我曾经踩过的坑:一开始我把所有RPC调用都录了,结果存储暴涨,回放时还出现了时序错乱。后来我只录导致Crash的那条关键链路——一般从用户点击到崩溃前最后10秒就够了。别贪多。
录制还需要注意几点:
- 敏感数据脱敏:用户名、密码、不能明文录,要hash或抹掉。
- 保留上下文:比如Session ID、traceId,回放时要能关联上。
- 标记Crash点:强烈建议在Crash发生时立即打标记,这样回放时能直接定位到爆炸的那一步。
阶段二:存储——选对序列化格式
存储格式我推荐两种:
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON | 可读性好,调试方便 | 体积大,序列化慢 | 少量用例、开发期 |
| Protobuf | 体积小,速度快,支持schema演进 | 可读性差,需要schema | 大规模流量录制 |
我个人习惯:开发阶段用JSON,方便手改数据;CI/CD里用Protobuf,能节省存储和网络开销。另外注意,存储时最好把timestamp和threadId一起记下来,因为多线程并发问题在回放时很容易暴露。
小技巧:每条记录加一个唯一RequestId,方便回放时与日志关联。我曾经靠这个把某个间歇性Crash从5秒定位缩短到30秒。
阶段三:回放——Mock的关键就在这里
回放时最怕什么?依赖不响应。你重放了一个请求,但数据库不在、缓存超时、第三方服务拒绝连接——Crash没复现,但系统挂了。
所以回放前必须把所有外部依赖Mock掉。我通常用MockServer或WireMock来搭建假服务。
// 以WireMock为例,录制阶段已经生成了stub文件
WireMockServer server = new WireMockServer(options().port(8080));
// 加载录制的stub映射
server.loadMappingsFrom("recorded_stubs/");
server.start();
// 回放时,被测系统所有请求都会打到WireMock上
// WireMock会按照录制时的响应返回
这里有个很关键的细节:回放顺序要严格保持录制时的先后。比如录制时请求A先于请求B,回放时如果顺序颠倒,可能触发完全不同的状态,Crash可能就复现不了了。
你想想看:用户先点了登录(写Cookie),再点支付(读Cookie)。如果你回放时先放支付再放登录,Cookie读不到,Crash就不出现了——这就是假阴性。所以我回放时一定会用队列保证顺序。
阶段四:比对——找Crash,也找差异
回放结束不等于测试结束。最关键的是对比回放结果与录制结果。比对维度包括:
- 返回值是否一致 —— Mock过的服务返回肯定是固定的,但被测系统自身的返回值可能因时间、随机数变化。我一般允许一定容差。
- 异常/错误码是否匹配 —— 如果录制时抛了
NullPointerException,回放时也应该是同一个异常。 - Crash堆栈是否一致 —— 这是我们的核心关注点。堆栈的类名、行号可以不完全一样(因为代码更新),但异常类型和调用链应该相似。
- 额外副作用 —— 比如回放后是否多写了日志、多发送了消息,这些可能是Bug的前兆。
// 比对逻辑伪代码
boolean match = true;
if (!expectedException.equals(actualException)) {
match = false;
logger.error("异常不匹配: expected={}, actual={}", expectedException, actualException);
}
if (!expectedStackTrace.contains(actualStackTrace)) {
match = false;
logger.warn("堆栈有偏差,需人工审核");
}
if (match) {
logger.info("Crash复现成功,比对通过");
} else {
logger.warn("Crash可能未复现或被新代码修复");
}
我遇到过很坑的情况:有一次回放时Crash没出现,我以为Bug被修了。结果上线后又炸了。后来发现是Mock时把一个输入参数类型改错了,导致异常路径没走到。所以比对时不能只看有没有Crash,还要看执行路径是否一致。建议回放时也把日志级别提到DEBUG,对比关键log。
几个核心原则总结
写到这里,我把Mock回放测试的几个核心原则列出来,你们平时做测试时可以直接当checklist用:
- 隔离性:回放环境必须和生产隔离,不能修改真实数据。
- 确定性:在Mock不变、输入不变的情况下,每次回放结果必须一致。
- 顺序性:多请求场景严格按录制时序重放。
- 可追溯:每条回放记录都能对应到录制时的原始请求,方便排查偏差。
- 最小化:只录制和回放与Crash相关的流量,不要贪多嚼不烂。