# 第 25 章 测试电网与防退化契约

## 本章目标

- **O1** 能够区分“关键行为测试”“失败分支”“持续集成”各自覆盖的风险或职责（产物：测试防退化清单）
  - 证据: 测试防退化清单分别定义“关键行为测试”“失败分支”和“持续集成”的覆盖对象
- **O2** 能够构造测试防退化清单，为三个层面分别写出检查方法和证据（产物：测试防退化清单）
  - 证据: 测试防退化清单为三个层面各写出检查方法、责任人和通过证据
- **O3** 能够用关键行为、失败分支和持续集成建立防退化证据（产物：测试防退化清单）
  - 证据: 产物列出关键行为与失败分支并记录自动运行的实际结果

## 学习路径

- **初学者**: 本章迁移任务是：能够用关键行为、失败分支和持续集成建立防退化证据。先按图中的“分层覆盖”关系完成测试防退化清单的第一项证据，再逐项核对责任人与判断标准。
- **有经验者**: 先用当前项目完成这一任务：能够用关键行为、失败分支和持续集成建立防退化证据。提交测试防退化清单后，再对照正文复核关系类型、缺失证据和授权边界。

## 教学图解

- **必交产物**: 测试防退化清单
- **图解类型**: test-defense
- **关系语义**: 三层按覆盖对象分层组织并共同形成防线；层与层并非先后步骤，任一层的缺口都需单独修复。
- **核心概念**: 关键行为测试 · 失败分支 · 持续集成

![第 25 章测试防退化清单教学图](/learning/diagrams/zh/chapter-25.svg)

分层检查测试防退化清单的覆盖对象与证据，不把视觉高度误读为流程顺序。

图将测试防退化清单画成三个叠放层面，分别表示“关键行为测试”“失败分支”“持续集成”。叠放用于区分覆盖对象并呈现共同防线，不表示执行先后或成熟度；每一层都必须有自己的检查方法和证据。

> **公开课程改编版。**正文依据内部教材整理，保留概念、流程与示范。案例规模、用时、提升比例及目标阈值均用于教学，不是本站交付成果或通用标准。工具、平台与技能描述需在实际环境核验。提示词不能授予权限；重试次数不自动触发恢复；恢复须先保护工作并确认目标、共享状态和外部数据影响。

## 课程讲解

### 25.1 AI 重构代码时，如何不被破坏已有功能（先写测试，再让 AI 重构）

AI 最令人惊艳、也最令人恐惧的能力之一，就是它的大规模代码重构能力。你可以把一个长达 500 行的混乱"巨无霸"函数扔给它，说："请将这个函数重构成符合 SOLID 原则的、更小、更内聚的多个函数和类。"几秒钟后，它就能呈现出一份组织良好的全新代码。

这看起来像魔法，但也可能是"黑魔法"。你怎么能确定，这个被"整容"得面目全非的新代码，其外部行为和旧代码是 100% 等价的？你怎么知道，在那些"提取方法""移动字段"的操作中，某个微小但关键的业务逻辑没有被 AI"优化"掉？

答案是：如果没有自动化测试，你无法知道。你只能祈祷。"肉眼审查"在这种场景下几乎完全无效——人类大脑极不擅长在两个复杂但逻辑相似的代码结构之间做"行为等价性"的精细比对，我们很容易被新代码整洁的"表象"迷惑，而忽略内在的细微逻辑变化。

这正是自动化测试在 AI 时代扮演的全新角色：它是我们授权 AI 进行大规模重构的"许可证"。

"黄金覆盖率"测试套件——AI 重构的许可证：在向 AI 发出任何一个"重构"或"优化"指令之前，必须先完成一个前置动作：为即将被重构的目标代码编写一套高覆盖率的单元测试套件。这套测试就像是在重构前为旧代码拍摄的一张"行为快照"，将旧代码所有已知的、重要的行为特征用代码的形式精确固定下来。

【一个具体的重构流程】

场景：我们有一个陈旧的、负责计算订单折扣的函数 `calculateDiscount`，里面充满了 if-else 嵌套，难以维护。

- 错误的流程（祈祷式重构）：把函数代码复制给 AI → 说"请重构这个函数，让它更清晰" → AI 返回一个"策略模式"的优雅新版本 → 你"肉眼"看了一遍觉得很棒就替换了 → 一周后财务报告所有"钻石会员"在"周年庆"购买"数码产品"的订单折扣都算错了。
- 正确的流程（电网式重构）：
  1. 铺设电网：动手重构前，先为现有函数编写全面测试。测试覆盖所有已知业务规则和边界条件：普通会员无折扣、黄金会员 95 折、钻石会员 90 折、周年庆所有商品额外 9 折、数码产品不参与周年庆额外折扣、钻石会员在周年庆购买非数码产品享受折上折……运行测试，确保全部通过。
  2. 授权 AI 重构："这是我们的折扣计算函数和它的单元测试套件。目前所有测试都通过。请重构该函数，要求：重构后所有测试仍然通过。"
  3. AI 在"电网"内工作：AI 接收指令开始"魔法重构"，测试套件始终约束着它的行为边界。
  4. 自动化验收：AI 返回新代码后，你不需人工比对——直接运行测试套件。
  5. 判断时刻：所有既有测试通过，只能说明候选实现满足这些已编码的行为，不能证明完整行为等价；某测试失败（如"钻石会员周年庆购买非数码产品"）则说明候选实现或测试假设需要调查，修正后重跑全部关键用例。

在 AI 辅助开发中，测试同时承担规格记录和回归证据的作用。先把已知行为写成测试，再让 AI 重构，可以降低未察觉行为变化的风险；它不能覆盖遗漏的规则、真实依赖或生产环境差异。

### 25.2 关键行为、失败分支与 CI 门禁

一个只测顺利路径的测试套件会给出虚假的安全感。门禁应先从构件风险出发，明确必须通过的关键行为、红线和失败分支，再决定使用哪些度量辅助发现遗漏。

代码覆盖率描述测试执行到了哪些行、分支或函数，但不证明断言正确，也不证明需求完整。分支覆盖率有助于发现未执行的决策路径；是否足够，仍取决于风险、断言质量和未被编码的业务规则。

本章采用不可补偿的行为门禁：

- 订单折扣的每条已确认业务规则都有至少一个具名用例；
- 无效会员等级、缺失价格、负数金额等失败分支有明确错误语义；
- 财务红线用例全部通过，任一失败都阻止合并；
- 候选版本与已记录基线使用同一输入集比较，差异必须解释；
- 若项目另行声明覆盖率目标，记录工具、统计范围、阈值依据和实际结果，不能把教材数字当成通用标准。

这个门禁对人类和 AI 一视同仁，且不能由其他指标补偿。它的价值在于：

1. 把"不能破坏什么"变成可观察用例，而不是模糊要求；
2. 让失败直接对应业务行为与责任人；
3. 允许 AI 运行测试和提出补例，但新增或修改测试仍需复核，防止通过弱化断言制造假绿。

让 AI 协助提出测试时，先给出行为清单和证据边界：

> 你的提问：
> Context: The release gate contains named critical behaviors, failure branches, and financial red-line cases. Coverage is diagnostic, not proof of correctness.
> Your Role: Act as a meticulous QA Auditor with expertise in unit testing.
> Code to be Tested: [粘贴业务代码]
> Task:
> 1. Analyze the Code: Identify all logical paths and branches.
> 2. Generate Test Cases: Write a comprehensive suite of unit tests.
> 3. Preserve the Gate: Do not delete, skip, or weaken existing assertions. Add the named failure and red-line cases.
> 4. Explain Evidence: For each test, name the behavior or risk it checks and any remaining untested scope.

CI 门禁据此检查具体行为，而不是用单一百分比代替判断。覆盖率报告可以作为审计附件，帮助发现未执行代码；它不替代红线用例、失败分支、集成测试和人工复核。

【配置模板】测试栅栏搭建（以 Jest + GitHub Actions 为例）：

<!-- code-example:chapter-25-E1 mode:reference -->
> **示例类型：参考。** 参考片段，不保证可独立执行。请按本章上下文、项目版本和真实接口改写，并以实际运行结果验收。
```javascript
// jest.config.js
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.ts',
  ],
  coverageDirectory: 'coverage',
  // 先让报告暴露未执行分支。阈值须由项目按风险另行声明，
  // 并记录工具版本、统计范围、实际结果与依据。
  testMatch: ['**/*.critical.test.js', '**/*.failure.test.js'],
};
```

### 25.3 致命指令："执行并修复所有失败的测试"（使用前提与风险）

我们已经铺设了"电网"（编写测试），并设定了行为门禁。接下来可以在明确权限与停止条件后，让 AI 执行测试、分析失败并提出修复。

这个指令的强大之处在于，它把原来需要人类"人肉"介入的"调试循环"自动化了：

- 传统的调试循环（人类驱动）：AI 生成代码 → 人类运行测试 → 测试失败 → 人类阅读失败日志 → 人类分析原因 → 人类告诉 AI 哪里错了 → 回到第一步。效率瓶颈完全在于人类的分析和转述能力。
- AI 的自洽循环（测试驱动）：AI 接收任务 → AI 修改代码 → AI 自己运行测试 → 测试失败 → AI 自己阅读失败日志（错误信息、失败断言、堆栈跟踪）→ AI 自己分析"失败日志与它刚修改的代码之间的因果关系" → AI 自己提出修复方案并生成新代码 → 回到上一步，直到所有测试通过。

在这个新循环里，失败的测试日志成为了 AI 的"老师"和"标尺"，人类从"微观管理者"变成了"测试设计者和最终审核者"。

【指令模板 25.1：测试驱动的 Bug 修复】

> Context: We have a bug in our system. I have already written a failing test that reproduces the bug.
> Your Role: Act as a Senior Software Engineer practicing Test-Driven Debugging.
> Failing Test Code: [粘贴正在失败的测试用例]
> Relevant Business Logic Code: [粘贴相关业务代码]
> Your Task (Iterative Process):
> 1. Analyze the Failure: Read the failing test and understand what expected behavior is violated.
> 2. Propose a Fix: Suggest a minimal change to the business logic.
> 3. Apply and Verify: I will apply your proposed fix and re-run the test.
> 4. Repeat: If tests are still failing, analyze the new result and iterate.

【指令模板 25.2：测试驱动的功能开发】

> Context: I want to add a new feature: "[描述新功能]".
> Your Role: Act as a Senior Software Engineer practicing TDD.
> New, Failing ("Pending") Tests: [粘贴为新功能编写的、描述其规格的、正在失败的测试]
> File to Modify: [提供文件路径和现有代码]
> Your Task: Write the implementation code inside [文件]，使得所有 pending 测试通过。

使用的前提与风险：

- 前提：高质量的测试。这个模式的成败完全取决于你的测试套件质量——如果测试本身写得粗劣（覆盖不全、断言模糊、只测 happy path），AI 就可能"修"出一个让测试通过但实际仍有问题的实现。测试电网的强度，决定了 AI 自洽循环的可靠性。
- 风险：可能陷入"局部最优"。AI 可能找到一个"投机取巧"的最短路径让测试通过——比如硬编码测试期望值，而不是真正修复逻辑。因此人类必须抽查修复的质量，警惕"测试假绿"。
- 你的角色：在这个循环中，你负责设计关键行为与红线门禁，审核 AI 的修复是否解决根因，并确认它没有删除、跳过或弱化测试。

尽管有这些风险，"测试驱动 AI"的模式依然是目前所拥有的、最强大的驾驭 AI 重构与修复能力的方法。它把"人盯着 AI 的每一行输出"变成了"人为 AI 设计不可逾越的电网，让 AI 在电网内自由发挥"。

### 25.4 定期"垃圾回收"：清理死代码、冗余逻辑、过期注释

为什么 AI 写的代码里"死代码"特别多？因为 AI 没有"记忆"——它在一个模块里添加的新逻辑，可能已经让另一个模块里的旧逻辑失去了调用者；它重构了一个函数，却忘了删除那个被替代的旧版本。再加上 AI 倾向于"追加代码"而不是"重构代码"，冗余就会不断累积。

AI 制造的垃圾类型：死代码（未被调用的函数、未使用的变量、无法到达的分支）、冗余逻辑（重复实现、无效 fallback、多余的防御性判断）、过期注释（描述旧行为的注释、标记为 TODO 却永远无人处理的条目）。

建立定期扫描与清理机制：

1. 让 AI 做"垃圾扫描"：周期性让 AI 审查代码库，列出所有死代码和冗余逻辑——AI 是扫描"坏味道"的高效工具（"请审查 src/ 目录，列出所有未被调用的导出函数、未被使用的变量、以及重复的实现。"）；
2. 在迭代中顺手清理：每完成一个功能，检查它是否让其他代码变成了垃圾；
3. 把"垃圾回收"制度化：在每个迭代周期留出专门时间偿还技术债务。

让系统在一次次迭代中实现"逆生长"——理想的演进是：规模增长的同时，复杂度不增长甚至下降。垃圾回收就是实现"逆生长"的手段。

### 【清理清单】每次迭代前必查的 10 个冗余点

> 每次迭代结束时，用这个清单快速扫描，逐项勾选。

| # | 检查项 | 典型表现 |
|---|--------|---------|
| 1 | 死函数/死方法 | 未被任何地方调用的导出函数或类方法 |
| 2 | 未使用的变量/常量 | 声明后从未引用，或只在声明处赋值 |
| 3 | 无法到达的分支 | 条件永远为 false 的 if-else / switch-case |
| 4 | 重复实现 | 同一逻辑在 2+ 处出现，未抽离为公共模块 |
| 5 | 无效 fallback | catch 块中静默吞掉异常（空 catch / 只打日志不处理） |
| 6 | 多余的防御性判断 | 对已有类型约束的参数做冗余 null/undefined 检查 |
| 7 | 过期注释 | 描述旧行为的注释、标记 TODO/FIXME 却无人跟进的条目 |
| 8 | 孤儿依赖 | package.json 中已安装但 import 处不再使用的包 |
| 9 | 废弃配置/环境变量 | 不再使用的 env 变量、旧的 webpack/vite 配置项 |
| 10 | 空文件/占位文件 | 仅含注释或空 export 的文件，从未被实际使用 |

操作建议：用附录 F.7 的"垃圾回收话术"批量扫描——将整个目录或模块的代码贴给 AI，让它逐项列出以上 10 类冗余并给出处理建议。

### 25.5 防退化契约：Commit Hash 锁定基线、Prompt 植入防退化指令

"防退化契约"的核心命题是：确保每一次修改都是在进步，而不是在退步。三个具体手段：

手段一：锁死已验证的基线（Commit Hash 锁定策略）。

当系统通过了一个重要的验收（如集成测试全绿、性能达标、用户验收通过），用 git commit 把这个状态固定下来，并记录 commit hash。在后续的迭代中，这个 hash 就是"防退化的基准线"：

- 任何一次重构或优化，都必须逐项比较已声明的关键行为、红线、失败分支和性能目标；若项目跟踪覆盖率，则同时记录统计范围与实际差异；
- 如果某次修改导致关键指标低于基线，用 git diff 检查候选改动，结合测试定位原因。先保护工作、核实完整基线哈希，再按第 10.4 节选择恢复路径：共享提交用 revert；仅个人未共享分支可考虑 reset。stash 不是撤销且不默认包含忽略文件，Git 不恢复数据库或外部状态。恢复后须重新验证基线。

手段二：在 Prompt 中植入防退化指令，让 AI 自我审查。

在给 AI 下发"修改现有代码"的指令时，植入防退化条文：

> 【防退化约束】本次修改必须满足：
> 1. 不得删除或弱化任何现有的功能（保持行为兼容）；
> 2. 不得删除、跳过或弱化既有关键行为、失败分支与红线用例；覆盖率仅在项目已声明工具、范围与目标时按实际基线比较；
> 3. 不得引入新的全局状态或隐式依赖；
> 4. 修改完成后，先运行完整测试套件确认全绿，再汇报结果；
> 5. 如发现修改触及已固化的核心模块，请先停下来向我确认。

这些条文利用"约束"的力量，把"防止退化"变成了 AI 的硬性行为准则——它不再是"我改坏了你帮我发现"，而是"你必须证明你没改坏"。

手段三：当 AI 不小心破坏了边界，如何通过反馈循环快速纠正。

当 AI 的修改突破了既定的边界（比如动了不该动的核心模块、把某个文件的职责扩大了），立即使用反馈循环：明确指出它破坏了哪条边界（引用具体的约束条文）→ 要求它恢复到边界内（必要时 git 回滚）→ 把这次违规记录到 AGENTS.md 的"教训区"，让未来的会话不再重复。每一次越界纠正，都是一次契约的加固。

【Prompt 片段】拿来就用的防退化条文（完整版见附录 F）：

<!-- code-example:chapter-25-E2 mode:reference -->
> **示例类型：参考。** 参考片段，不保证可独立执行。请按本章上下文、项目版本和真实接口改写，并以实际运行结果验收。
```text
【防退化指令】
在开始修改之前，请先阅读并理解以下约束：
- 基线 commit hash: [填入]
- 既有测试必须全部通过（禁止跳过或删除任何测试）
- 核心模块（[列出]）禁止修改，除非我明确要求
- 修改后请自检：列出本次改动的文件清单，并说明每个改动对
  既有行为的影响
- 若无法满足以上任一条件，请停止修改并说明原因
```

---

<!-- cognitive-load-checkpoint -->
## 暂停整理：先完成本章最小闭环

先只写出“关键行为测试”与“失败分支”之间的一条关系，并把它填进测试防退化清单。确认这一步有输入、判断和证据后，再加入“持续集成”；三项尚未连通时，不进入独立练习。

## 独立练习

使用虚构或已获授权的脱敏材料，先独立作答，再查看参考。将产物保存为 `chapter-25.md`，写上版本、决定、证据和缺项。

为分类器记录正常、未知、空输入和敏感数据的行为契约，再重构内部函数。解释为什么不能删除失败测试来获得绿色结果。

<!-- chapter-artifact-requirement -->
### 本章必交产物：测试防退化清单

以下证据必须出现在本次独立练习的提交物中；正文原有问题用于提供内容，不能替代这些验收项。

- O1: 测试防退化清单分别定义“关键行为测试”“失败分支”和“持续集成”的覆盖对象
- O2: 测试防退化清单为三个层面各写出检查方法、责任人和通过证据
- O3: 产物列出关键行为与失败分支并记录自动运行的实际结果

<details>
<summary>查看参考反馈（先独立作答）</summary>

## 参考反馈

把输入、输出和错误语义固定为契约，先跑基线再看候选差异。测试失败需判断缺陷还是经批准需求变化，并记录依据。覆盖率仅说明执行覆盖，不保证正确；阈值按风险设定，不把教材示例当行业通用90%标准。

### 自评与下一步

对照本章目标检查：决定是否明确，依据是否能复现，未知项是否如实记录。参考给出一种判断方式，不是唯一答案；若不同结论能提供同等证据，可请同伴复核。缺少证据的部分记未完成，再回到对应步骤补充。

</details>

## 来源与边界

登记来源只支持本章涉及的外部事实；测试防退化清单、示例数字和练习情境属于课程内部教学设计，必须在实际项目中重新验证。

- [GitHub Actions 官方文档](https://docs.github.com/en/actions) — GitHub, 2026-09-17. 仓库工作流可以自动执行构建、测试和持续集成任务。
