把测试通过变成验收证据

把测试通过变成验收证据

“测试通过了”不是证据。

它最多是证据的标题。

真正有用的测试证据,要能回答:

  • 测了什么行为?
  • 用了什么输入?
  • 控制了哪些依赖?
  • 覆盖了哪些路径?
  • 命令是什么?
  • 输出是什么?
  • 哪些风险还没测?
  • 这个结果能不能支持验收?

如果这些问题答不上来,一句“测试通过”并不能让系统更安全。

一、测试不是越多越好,而是越贴边越好

很多项目会陷入一种焦虑:测试覆盖率越高越安全。

覆盖率有价值,但覆盖率不是安全本身。你可以写很多不痛不痒的测试,也可以漏掉真正会让线上出事的路径。

测试要贴着问题定义、设计边界和失败模式。

一个改动如果是修复状态写回失败,最重要的测试不是“导出成功”。最重要的是:

  • 上传成功但状态写回失败时,任务是否保留可恢复状态。
  • 重试是否幂等。
  • 重复触发是否不会生成两个导出文件。
  • 状态为空或历史状态不认识时是否降级。
  • 依赖失败时是否留下可观测错误。

这叫贴边。

二、把测试结果写成台账

我现在更喜欢用测试证据台账,而不是只写一段测试总结。

格式可以很简单:

1
2
3
4
5
6
| 链路 | Path | 输入 / Fixture | 命令 | 结果 | 证据 | 风险 |
| --- | --- | --- | --- | --- | --- | --- |
| 导出成功 | happy | articleWithBody | npm test export.success | pass | terminal log | 无 |
| 缺 token | nil | expiredSession | npm test export.auth | pass | terminal log | 只覆盖 API 层 |
| 空文章 | empty | emptyArticle | npm test export.empty | pass | golden diff | 未覆盖 UI |
| 上传超时 | error | fakeUploaderTimeout | npm test export.timeout | pass | terminal log | 未跑真实存储 |

这张表的价值不是好看,而是让验收者能迅速判断:证据够不够支持发布。

它把“测试通过”拆成了具体问题:

  • 通过的是哪条链路?
  • 属于 happy、nil、empty、error 哪个路径?
  • 输入是什么?
  • 用什么命令验证?
  • 证据在哪里?
  • 风险是否仍然存在?

三、没有测到的也要写

成熟的测试证据不是假装覆盖一切,而是诚实写出没覆盖什么。

例如:

1
2
3
4
Not Covered:
- 未跑真实对象存储,只用 fake uploader 验证超时语义。
- 未跑旧客户端回归,当前根据接口兼容性分析判断风险低。
- 未跑高并发重复导出,记录为后续性能测试。

这比一句“测试通过”有价值得多。

未覆盖风险不是失败。隐藏未覆盖风险才是失败。

工程里真正危险的不是“我们知道这个风险还没测”,而是“大家以为它测过了”。

四、区分 Test Plan 和 Test Blueprint

一个常见混乱,是把“未来想测什么”和“现在已经测了什么”混在一起。

所以要区分 Test Blueprint 和 Test Plan。

Test Blueprint 是目标态自动化蓝图。它可以写:

  • 将来应该补哪些层级的测试。
  • 哪些路径适合单元测试。
  • 哪些路径适合集成测试。
  • 哪些路径需要 UI 自动化。
  • 哪些路径只能人工验收。

Test Plan 是当前可执行验证。它必须写:

  • 本次实际跑哪些命令。
  • 预期结果是什么。
  • 实际结果是什么。
  • 证据在哪里。
  • 哪些没测。

Blueprint 可以理想,Plan 必须诚实。

如果把 Blueprint 当成 Plan,你会得到一种虚假的安全感:文档里看起来什么都覆盖了,但实际上当前没有一条证据。

五、Fixture、Mock、Stub、Fake 的边界

测试证据链离不开依赖控制。

Fixture 是测试输入和环境样本。

坏 fixture 叫:

1
2
3
user1
article1
payload1

好 fixture 叫:

1
2
3
4
5
loggedInUser
articleWithLongBody
emptyArticle
expiredSession
exportTaskUploadedButStatusNotPersisted

命名本身就是测试文档。

Stub 提供固定返回,适合控制简单依赖,比如配置读取、时间、当前用户。

Mock 关注交互是否发生,适合验证“是否调用了某个依赖”“调用参数是什么”“调用次数是否正确”。Mock 用多了会绑死实现细节,所以要谨慎。

Fake 是可工作的轻量实现,比如内存数据库、内存队列、临时文件系统、假的上传服务。Fake 通常比 mock 更适合测试状态流,因为它允许行为自然发生,而不是只验证调用。

选择原则:

  • 想控制输入,用 fixture。
  • 想固定返回,用 stub。
  • 想验证交互,用 mock。
  • 想模拟真实行为,用 fake。

六、验收不是跑完测试

测试通过只是验收材料之一。

验收要回答更大的问题:

  • 本次目标是否完成?
  • In Scope 是否全部覆盖?
  • Out of Scope 是否没有被带入?
  • 关键路径是否验证?
  • 四路径是否覆盖?
  • 哪些风险没有覆盖?
  • 是否需要发布?
  • 如果发布,如何观察?
  • 如果失败,如何回滚?

也就是说,验收是把需求、设计、测试、review 和发布风险收束到一个判断。

验收结论最好不要只有“通过 / 不通过”。

可以分成三类:

  1. Accepted for current scope。
  2. Accepted with residual risks。
  3. Not accepted; requires fix。

第二类很重要。很多真实任务不是零风险,而是在明确风险后接受。

例如:

1
2
3
4
5
6
Accepted with residual risks.

Reason:
- 本次覆盖了 service 层 happy / nil / empty / error。
- 未跑真实对象存储集成测试,但 fake uploader 覆盖了超时语义。
- 由于本次改动不改变存储接口,接受该风险。

这比“通过”更诚实,也更适合复盘。

七、发布前还要问观察和回滚

如果一个改动会影响用户、数据、接口、配置、队列、存储、权限、支付、同步或后台任务,就不能只靠测试结束。

发布前还要问:

  • 发布后观察多久?
  • 看哪些指标?
  • 看哪些日志?
  • 哪些用户路径要冒烟?
  • 什么条件触发回滚?
  • 回滚命令是什么?
  • 回滚会不会破坏新数据?

没有观察窗口的发布,本质上是在靠感觉。

一个更好的发布观察写法是:

1
2
3
4
5
6
7
8
Observe Window: 发布后 30 分钟
Signals:
- export_job_failed_total 不高于过去 7 日同时间均值 20%
- export_job_retry_total 无异常尖峰
- status_persist_failed 日志不连续出现
Rollback Trigger:
- 导出失败率连续 10 分钟超过基线 2 倍
- 出现重复导出文件

这才是把发布拉进控制面。

八、完成必须带证据

任务结束时,最好的收尾不是“已完成”,而是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Changed:
-

Validated:
-

Not Covered:
-

Residual Risks:
-

Follow-up:
-

这几项看起来普通,但它们能显著减少后续扯皮和返工。

“测试通过”是一句话。
“验收证据”是一组可复查事实。

AI 时代代码会来得越来越快。越是这样,我们越需要把测试从一个动作,升级成一条证据链。