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/Stub 外部服务 支付宝、短信、邮件 保证依赖行为可控 时间戳对齐 冻结/偏移系统时间 保证数据时间一致 避免时间错乱 三者配合流程 1. 先拍状态快照 → 2. Mock 掉外部依赖 → 3. 对齐时间戳 → 4. 执行测试 测试结束后:恢复快照 → 清理 Mock → 时间自动恢复

你看,这三者是一个完整的闭环:

  1. 状态快照保证「环境干净」
  2. 依赖模拟保证「行为可控」
  3. 时间戳对齐保证「逻辑正确」

缺一个,场景复现就可能失败。我见过最惨的一次:快照做了,Mock 也做了,但时间没对齐。结果测试跑了 3 小时,全绿。上线后,所有定时任务全部错乱。嗯,从那以后,我再也不敢漏掉时间戳对齐这一步了。

一句话总结:场景复现 = 状态快照(环境) + 依赖模拟(行为) + 时间戳对齐(逻辑)。三个都做到位,Bug 想跑都跑不掉。

可可精品资料库