Mock回放技术原理
要理解 Mock 回放,得先搞清楚它到底在干什么。说白了,就是把真实服务的“行为”录下来,然后在测试环境里“放”回去。我最早接触这个思路时,觉得不就是个录音机吗?后来踩了几个坑才明白——真正的难点不在录,而在「放得准」。
录制/回放模式
录制阶段,我们启动一个代理程序,拦截被测系统发出的所有 HTTP 请求。代理会把请求的完整信息——URL、Headers、Body、甚至请求顺序——连同后端返回的响应一起,存到一个“磁带”里。这个磁带可以是文件、数据库,或者内存缓存。
| 模式 | 动作 | 产出 |
|---|---|---|
| 录制 | 拦截请求 + 保存请求/响应对 | 磁带文件(如 .yaml / .json) |
| 回放 | 读取磁带 + 匹配请求 + 返回预存响应 | 模拟的服务端响应 |
回放时,代理不再转发请求到真实后端,而是直接在本地磁带里找匹配项。找到就返回对应响应,速度比真实调用快几十倍。没找到?那就抛出异常——我习惯在 CI 里让这种失败直接打断流水线,避免潜藏问题。
个人经验:录制时最好固定种子数据。我在项目里吃过亏——第一次录的磁带里有个随机ID,回放时参数变了,怎么都匹配不上。后来老老实实把随机值替换成占位符,问题才解决。
# 录制配置示例(以 pytest-recording 插件为例)
import vcr
@vcr.use_cassette('fixtures/vcr_cassettes/test_api.yaml')
def test_get_user():
response = requests.get('http://localhost:5000/api/user')
assert response.status_code == 200
请求匹配
匹配是回放的核心。系统拿当前请求跟磁带里所有已录请求对比,找到最相似的那个。常用的匹配策略有这几种:
- 精确匹配:URL + Method + Headers + Body 完全一致。最严格,但稍变字段就会失败。
- 模糊匹配:只匹配 URI 和 Method,忽略 Headers 和 Body。宽松,但易误匹配。
- 参数匹配:指定关键字段必须一致,其他可忽略。我常用这种方式,平衡准确与鲁棒性。
注意:我曾经遇到一个问题——同一个 API 被调用了两次,但第二次请求里带了一个时间戳参数。因为没在匹配规则里排除该参数,回放时永远找不到第二条记录,导致测试一直失败。解决方案是用
match_on 参数只匹配 URL 和 Method。
# 自定义匹配规则
with vcr.use_cassette('my_cassette.yaml',
match_on=['uri', 'method']):
# 回放时只会检查 URI 和 HTTP 方法
response = requests.post('http://api.example.com/data',
json={'key': 'value'})
还有一点容易忽略:请求顺序。如果测试里依次调用了 A、B 两个接口,但磁带里录的顺序是 B、A,回放时也会按磁带顺序匹配。我在一个微服务项目里就碰到过这种“交叉匹配”的坑——两个接口的请求参数差不多,结果 A 的请求匹配到了 B 的响应,数据全错了。
响应模拟
匹配成功之后,系统从磁带里取出对应的响应数据,包括状态码、Headers、Body。这一步看似简单,但有几个要点:
- 延迟模拟:真实服务的响应时间往往在 30~500ms,但 Mock 回放几乎瞬间返回。如果你想测试超时重试逻辑,需要手动注入延迟。我习惯用
vcr.record_mode='once'配合Cassette.play_count来模拟不同场景。 - 动态替换:磁带里的响应可能是静态的,但有些字段(如 session )需要动态生成。可以在录制后对磁带做后处理,把固定值替换成表达式。
- 错误场景:别只录正常响应。我建议至少录三个版本的磁带:正常、超时、500 错误。回放时通过环境变量切换,能覆盖更多边界。
核心原则:Mock 只模拟行为,不模拟状态。如果业务依赖数据库中的连续状态(比如购物车物品累积),最好把状态逻辑也录进磁带,或者改用带状态的 Mock Server。
回头再看这张流程图,你会发现整个原理其实不复杂。但每个环节都有隐藏的坑。我自己的经验是:录制时多想一步,回放时就能少改一次。别偷懒,把需要排除的参数、需要注入的延迟、需要替换的动态值,都在录制阶段处理好,后期 CI 跑起来才真的省心。