4、场景复现技术:状态快照与恢复、依赖模拟(Mock/Stub)、时间戳对齐
场景复现,说白了就是让 Bug 能「召之即来」。
我做了这么多年测试,最怕听到的一句话就是:「那个 Bug 我昨天还能复现,今天怎么不行了?」。嗯,这种问题往往不是开发在甩锅,而是我们的测试环境太「脏」了——状态乱了、依赖挂了、时间对不上了。
这一节,我们就来聊聊怎么把场景「冻住」、「骗过去」、「对齐了」。三个技术,搞定 90% 的复现难题。
4.1 状态快照与恢复
先问一个问题:你测一个支付流程,测到一半,数据库里多了条订单,缓存里多了个 token,文件系统里多了张图片。然后你想重新测一遍——怎么办?
最笨的办法:手动清理。我见过有人每天上班第一件事就是跑 20 条 SQL 删数据。说实话,这效率太低了。
更好的办法:状态快照。
核心思路:在场景开始前,把系统所有关键状态「拍一张照片」。测试结束后,一键恢复。
哪些状态需要快照?我列个清单:
| 状态类型 | 典型内容 | 快照方式 |
|---|---|---|
| 数据库 | 用户表、订单表、配置表 | mysqldump / pg_dump / MongoDB 快照 |
| 缓存 | Redis 中的 session、计数器 | SAVE / BGSAVE / RDB 文件 |
| 文件系统 | 上传的图片、日志、临时文件 | tar / rsync / 目录快照 |
| 内存状态 | JVM 堆、Python 进程变量 | jmap / pickle / 序列化 |
| 容器状态 | Docker 容器、K8s Pod | docker commit / checkpoint |
我个人习惯用 Docker 做快照。举个例子:
# 拍快照
docker commit my_service my_service:snapshot_before_test
# 跑测试
pytest test_payment.py
# 恢复快照
docker stop my_service
docker run --name my_service_restored my_service:snapshot_before_test
我在项目中遇到过一个问题:数据库快照太大,每次恢复要 5 分钟。后来怎么解决的?只快照「关键表」,不拍全库。你想想看,用户表、订单表、配置表才多大?日志表、流水表动辄几千万行,根本没必要拍。
小技巧:快照前先做一次「最小数据集」裁剪。只保留场景依赖的数据,其他统统删掉。恢复速度能快 10 倍。
4.2 依赖模拟(Mock / Stub)
场景复现最头疼的是什么?外部依赖不可控。
你测支付,支付宝接口今天返回成功,明天返回失败,后天直接超时。你怎么办?等支付宝出 Bug 吗?不现实。
所以我们要「模拟依赖」——把外部服务替换成我们自己控制的假服务。
这里有两个概念容易混淆:Mock 和 Stub。我简单解释一下:
- Stub(桩):一个固定的假实现。你调用它,它永远返回你预设好的结果。比如支付宝 Stub 永远返回「支付成功」。
- Mock(模拟对象):一个可编程的假对象。你不仅能预设返回值,还能验证调用次数、参数、顺序。比如「这个 Mock 的 send_sms 方法必须被调用 1 次,且参数 phone 必须是 138xxxx」。
说白了,Stub 是「死数据」,Mock 是「活验证」。
实战中怎么用?我拿 Python 的 unittest.mock 举个例子:
from unittest.mock import Mock, patch
# 场景:测试订单超时取消逻辑
# 依赖:支付宝查询接口
def test_order_timeout_cancel():
# 创建一个 Mock 支付宝客户端
mock_alipay = Mock()
mock_alipay.query_order.return_value = {
"status": "WAIT_BUYER_PAY",
"timeout": True
}
# 用 patch 替换真实依赖
with patch('my_service.alipay_client', mock_alipay):
result = cancel_timeout_order("order_123")
assert result == "cancelled"
# 验证 query_order 被调用了 1 次
mock_alipay.query_order.assert_called_once_with("order_123")
我曾经踩过一个坑:Mock 了数据库,结果测试全过了,上线后数据库连接池爆了。为什么?因为 Mock 没有模拟「连接超时」这个行为。所以我的建议是:Mock 要模拟「正常」和「异常」两种路径。
避坑指南:不要过度 Mock。我曾经见过一个项目,Mock 了 90% 的依赖,结果测试覆盖率 95%,线上 Bug 率 50%。为什么?因为 Mock 出来的行为和真实行为差太远了。记住:Mock 只模拟「接口契约」,不模拟「内部实现」。
4.3 时间戳对齐
这个坑,我估计 80% 的测试都踩过。
场景:你测一个「24 小时未支付自动取消」的功能。你手动把系统时间改到明天,结果发现订单没取消。为什么?因为数据库里的 created_at 是今天的时间戳,系统时间却是明天的。时间对不上,逻辑就乱了。
时间戳对齐,说白了就是让「被测系统的时间」和「数据里的时间」保持一致。
常用的方法有三种:
| 方法 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 系统时间篡改 | 直接改操作系统时间 | 单机测试、无时间敏感服务 | 影响其他进程,容易出乱子 |
| 时间库 Mock | Mock 掉 datetime.now() 等函数 | 单元测试、集成测试 | 只能 Mock 代码层,管不到数据库 |
| 时间偏移注入 | 在请求/数据中注入时间偏移量 | 端到端测试、压测 | 需要框架支持 |
我个人最推荐第三种——时间偏移注入。为什么?因为它不影响系统其他部分,只影响当前测试的数据。
举个例子,我们用 Python 的 freezegun 库:
from freezegun import freeze_time
import datetime
# 场景:测试订单 24 小时后自动取消
@freeze_time("2024-01-01 10:00:00")
def test_order_auto_cancel():
# 创建订单,此时时间被冻结在 2024-01-01 10:00:00
order = create_order()
assert order.created_at == datetime.datetime(2024, 1, 1, 10, 0, 0)
# 快进 25 小时
with freeze_time("2024-01-02 11:00:00"):
result = check_and_cancel_order(order.id)
assert result == "cancelled"
你看,这样既不用改系统时间,也不用担心影响其他测试。而且 freeze_time 是上下文管理器,测试结束自动恢复,干净利落。
经验之谈:时间戳对齐最容易出问题的地方是「跨时区」。我建议所有测试数据统一用 UTC 时间,展示层再转本地时间。这样不管你在北京还是纽约,测试结果都一样。
4.4 三者如何配合?
这三个技术不是孤立的。我画了一张图,帮你理清它们的关系:
你看,这三者是一个完整的闭环:
- 状态快照保证「环境干净」
- 依赖模拟保证「行为可控」
- 时间戳对齐保证「逻辑正确」
缺一个,场景复现就可能失败。我见过最惨的一次:快照做了,Mock 也做了,但时间没对齐。结果测试跑了 3 小时,全绿。上线后,所有定时任务全部错乱。嗯,从那以后,我再也不敢漏掉时间戳对齐这一步了。
一句话总结:场景复现 = 状态快照(环境) + 依赖模拟(行为) + 时间戳对齐(逻辑)。三个都做到位,Bug 想跑都跑不掉。
可可精品资料库