适用读者:把"单测全绿"当成"完成"的开发者;用 AI 编程工具交付、需要验收判据的人。
前置:S16-B03、能启动一次完整应用(前端/后端/依赖服务)。
覆盖旧文:A-275 → 96 点(§3 harness 与架构规则、§4 代码示例);A-275 的"审查反馈提升"并入 A02。
扩展点:E-S16-004(E2E 最小集)。
上一篇:S16-B03 · 集成测试:Spring Boot Test 与 Testcontainers
下一篇:S16-J01 · 链路追踪基础。
1. 一句话结论
"做完了"必须由系统级证据定义,而不是由"我跑过单测"定义。 单元测试证明零件,端到端证明零件装在一起还能转——后者是发布前的唯一凭据。
| 声明 | 需要的证据 |
|---|---|
| "功能实现了" | 一条从入口到出口的脚本跑通,含断言 |
| "没破坏别的" | 同一套 E2E 全绿(回归) |
| "异常也处理了" | 注入故障(依赖超时/返回错误)仍按预期降级 |
2. 为什么单测会系统性漏(A-275)
原文的 Electron 案例:渲染进程组件、预加载脚本、服务层三层各自单测通过,agent 宣布完成;实际点击导出按钮:文件路径格式不对、进度条无反应、大文件导出内存泄漏——5 个组件边界缺陷,单测一个都没发现。
四类缺陷与成因:
| 缺陷类型 | 具体形态 | 单测为什么漏 |
|---|---|---|
| 接口不匹配 | 渲染进程传相对路径,预加载脚本要绝对路径 | 两侧各自 mock,假设不一致 |
| 状态传播错误 | 迁移改了表结构,ORM 缓存持旧结构 | 每个测试都是全新 mock |
| 资源生命周期 | 文件句柄/连接/socket 跨组件获取释放 | 测试各自创建销毁,不共享 |
| 环境依赖 | 配置差异、网络延迟、服务不可用 | 测试环境全 mock |
原文的判断:"只有端到端测试能证明系统级缺陷不存在。" 并且——当 agent 知道要过 E2E 时,它的编码行为会改变:会考虑上下游接口、会遵守架构边界、会处理错误路径。
3. 把架构规则变成可执行检查(§3)
原文的关键工程手法:规则不要写在文档里,要写成脚本,进 harness。
// tools/check-arch-rules.ts —— 渲染进程禁止直接调用 Node API
import { readdirSync, readFileSync } from "node:fs";
const dir = "src/renderer";
const bad = readdirSync(dir, { recursive: true })
.filter((f) => String(f).endsWith(".ts"))
.map((f) => [f, readFileSync(`${dir}/${f}`, "utf8")] as const)
.filter(([, src]) => /\brequire\(|from ["']node:/.test(src));
if (bad.length) {
console.error("架构违规:渲染进程直接引用 Node API");
for (const [f] of bad) console.error(" -", f);
process.exit(1);
}
console.log("架构规则通过");
预期结果:
$ node --experimental-strip-types tools/check-arch-rules.ts
架构规则通过
# 故意在 renderer 里写一行 import fs from "node:fs" 后:
架构违规:渲染进程直接引用 Node API
- main.ts
[exit code 1]
判据:这条规则能否在 CI 里以非零退出码挡住合并? 能 → 是规则;只能靠人 review → 是愿望。
4. harness 必须包含端到端层(§2)
| 层 | 是否在 harness 内 | 说明 |
|---|---|---|
| 单测 | ✅ | 快速反馈 |
| 组件/集成 | ✅ | 契约 |
| 端到端 | ✅ 必须有 | 唯一能证明系统级行为 |
| 人工点击 | ❌ 不能替代 | 不可重复、无断言 |
原文的 harness 定义是"可重复执行、带断言、失败即红"的集合;不是"一组手动步骤"。
5. E2E 最小集(E-S16-004)
| 场景 | 为什么必须有 | 断言点 |
|---|---|---|
| 主流程:登录 → 核心操作 → 结果可见 | 系统能不能用 | 终态数据 + 页面/接口返回 |
| 高价值异常:依赖失败时的降级 | 出故障时是否可用 | 响应时间、兜底值、错误码 |
| 边界数据:最大/空/重复提交 | 生产数据不听话 | 无 500、无脏数据、幂等 |
| (可选)性能基线 | 防止"功能对但慢 10 倍" | P95 < 阈值 |
数量控制:3–8 条。超过 10 条,维护成本会超过收益(B01 §3 的成本模型)。
6. 面向 agent(或新人)的错误消息(§3)
错误信息要让人/agent 能自己修,而不是只报"失败":
✗ e2e/export.spec.ts › 导出 100MB 文件
期望: dist/report.zip 存在且 < 50MB
实际: 文件缺失
上游线索: 渲染进程传入 path="report.zip"(相对路径),预加载脚本要求绝对路径
下一步: 在 renderer 中调用 path.resolve(dialogPath) 后再传给 preload
| 差的消息 | 好的消息 |
|---|---|
AssertionError: expected true to be true |
期望/实际 + 上游线索 + 下一步动作 |
| 只贴堆栈 | 指出哪个契约被破坏 |
7. 常见误区
| 误区 | 纠正 |
|---|---|
| "E2E 慢,用单测覆盖就行" | 慢的是数量,不是必要性;用 3 条主干路径换系统级证据 |
| "手动点一遍也算验证" | 不可重复、无断言、无法回归 |
| "有 E2E 就不用单测" | 反过来也不行:E2E 定位成本高,分支仍归单测 |
| "测试环境通过就等于生产通过" | 配置差异要单独列(B01 §2 环境依赖) |
8. 自测
- 你的项目现在有没有一条从入口到出口的自动化脚本?没有 → 先写主干那一条。
- 列出三条"如果坏了业务就停"的路径,它们是否都在 E2E 里?
- 你的一条架构规则能不能以非零退出码挡住合并?
9. 下一篇
S16-J01 · 链路追踪基础:Trace / Span / Parent Span —— 系统级证据的第二种形态:不是"跑通了",而是"跑的时候每一步花了多久、经过谁"。