# Tdd

> 当要用测试先行的方式落地一段行为改动时使用——如"用 TDD 实现这个功能、先写测试再写实现、红绿循环推进、修 bug 先写复现测试"。在约定 seam 上一次一个行为走红→绿→验证，并当场把命令与输出落盘成可核对的执行记录。composable 层能力单元，通常由 execute-task 在每个任务内调用。不要用于：给已写完的实现事后补测试、只跑测试不改代码、纯定位 bug 不改行为、代码评审、任务拆分。

- Skill: `rollrollroll/tdd` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add rollrollroll/tdd`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rollrollroll/tdd/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: RollRollRoll (https://skillmd.com/u/rollrollroll)
- Updated: 2026-08-19
- Page: https://skillmd.com/skills/rollrollroll/tdd

---


# TDD — 红 → 绿闭环 Skill（composable 层）

## 你的任务

在**已约定的 seam** 上，把一段行为改动做成一串**红 → 绿**闭环：一次一个行为，先写失败测试，
再写刚好让它过的最小实现，跑验证，**当场**把命令与输出写进执行记录。

你交付的不是"代码写完了"，而是**可核对的证据**：每个行为都有实现前的失败输出和实现后的通过输出，
收尾完整跑过一次验证命令。调用方（execute-task 或用户）拿这份证据判定"真的做完了"。

> 一句话：一个 seam、一个测试、一个最小实现，红了再绿，绿了留证。

## 调用契约

**传入**

- **需求依据**：要实现什么行为、验收标准、精确值（数字 / 签名 / 用例）。有任务简报就传路径，没有就直接说清。
- **seam**：测试写在哪个可观察边界上。**缺就先与调用方 / 用户确认**，不自己挑地方测。
- **验证方式**：这段改动跑什么命令验证（如 `pytest tests/test_x.py`）；项目有 typecheck 也一并给。
- **执行记录路径**（可选）：缺省由 `scripts/tdd-record.sh <名称>` 落在自忽略的 `.tdd/`；
  调用方要落在自己的工作目录时传第二个参数（execute-task 传 `task-N` 与 `.execute-task/`）。

**取回**

- **证据齐否**：每个行为是否都有红 + 绿，收尾验证命令是否完整跑绿。
- **改动文件清单**。
- **疑虑**：看到但按规则没动的结构问题。
- **记录路径**。
- 卡住时回 **BLOCKED** + 卡在哪，不硬猜往下写。

**本 skill 不做**

- 不 commit、不改 tasks checkbox、不动 git 状态——提交是调用方的闸门动作。
- 不排任务顺序、不决定做哪个任务、不改需求或技术决策。
- 不做整体代码审查（那是 **review-changes**）。
- 不在红绿之间夹带重构。

## 招牌机制：一次一片 + 当场留证 ⭐

TDD 的价值不在"有测试"，而在**测试先于实现存在过**——红是判据，绿才有意义。两条纪律撑住它：

### 一次一片（垂直切片）

一个 seam、一个测试、一个最小实现，然后下一个。每个测试都是 **tracer bullet**：
它回应上一轮学到的东西，而不是照着想象一次列完。
先写完所有测试再写所有实现是**水平切片**——验的是想象中的行为，对真实改动不敏感。

### 当场留证

每转一次红或绿，**立刻**把命令、输出、为何预期失败追加进执行记录。
跑完一整轮再凭印象补写的是回忆，不是证据；上下文一旦被压缩，只有落盘的那份还在。

> 检验：现在停下来，记录里能不能看出"这个行为在实现前真的红过"？看不出，就等于没走成 TDD。

## 工作流程

### 步骤 0 · 锁定 seam 与验证方式

seam 与验证方式**开工前必须都有**。调用方没给 seam → 现在就写下来与用户确认，
确认后再动手；不在未约定的接缝上写测试。

### 步骤 1 · 开记录

运行本 skill 目录下的 `scripts/tdd-record.sh <名称> [输出目录]`：它创建工作目录、
按标准结构建记录文件（已存在则不覆盖，便于续做）、打印路径。把 seam 与验证方式填进记录抬头。

### 步骤 2 · 逐行为红 → 绿

对每个行为：

1. **写失败测试** → 跑 → 看到红。
2. **确认红的原因是行为缺失**——语法错、import 错、环境没配好导致的红不算红过，先修掉再重跑。
3. **当场记红**：命令 + 失败输出关键几行 + 为何预期失败。
4. **写刚好让它过的最小实现** → 跑同一条命令 → 绿。不提前实现下一个测试要的东西、不加投机功能。
5. **当场记绿**：同一条命令 + 通过输出。
6. 下一个行为。

**重构不进循环**：让测试转绿的那一步不夹带结构改动。任务边界内的小整理，改完复跑覆盖测试；
结构性问题写进记录的「疑虑」交回调用方（execute-task 会把它带进整体验收的 architecture 轴）。

### 步骤 3 · 收尾验证

**完整跑一次**验证命令（加 typecheck / 相关测试文件），输出里的告警也要看见——
不拿红绿循环中途某一次的输出顶替，那时的代码还不是最终代码。完整输出写进记录的「收尾验证」。

### 步骤 4 · 交回

按下面「交付判据」自核，再把证据齐否 / 改动文件 / 疑虑 / 记录路径交回调用方。

## seam：测在哪，事先约定

- **seam** = 能从外部观察行为的公共边界（纯函数、模块接口、注入点）。测试住在 seam 上，不伸进内部实现。
- **不在未约定的 seam 上写测试**：seam 由上游设计 / 任务指定；没指明就先与用户确认再开工。
  测错地方比测得少更糟——本闭环里没有 reviewer 会替你发现测的是外壳还是内部实现。
- 难测的外壳（IO / 框架 / 网络）不硬测：把行为挤到 seam 内侧，外壳留薄。

## 好测试与三个反模式

好测试：**验行为不验实现** / **读起来像规格** / **期望值来自独立事实来源** / **能活过重构** /
**一个测试一个逻辑断言**。

三个反模式，认出来就换掉（识别信号与改法见 [references/test-quality.md](references/test-quality.md)）：

- **实现耦合**：mock 内部协作者、测私有方法、断言调用次数 / 顺序、绕过被测接口从侧信道验证。
- **同义反复**：期望值按代码的同一套算法重算一遍——它按构造必然通过，永远不可能与代码不一致。
- **水平切片**：先写完所有测试再写所有实现。

**mock 只在系统边界**：外部 API、时间 / 随机，（有时）数据库与文件系统。不 mock 自己的模块和内部协作者。

## 小步推进与验证节奏

- 不一次写完整功能；不堆到约 100 行才第一次跑测试。**每一步保持系统可构建、可测**。
- 未完成但需合并的功能，用 **feature flag** 挡住，别让半成品阻塞主干。
- 验证按从快到慢分层，快的频繁跑：**typecheck**（改完随手跑）→ **单测文件**（当前这一片）→
  **全套测试**（闭环末尾 / checkpoint）。

## bug 类改动：复现测试优先

1. 建立**紧凑反馈循环**（能快速复现）。
2. **复现并最小化**问题。
3. 提 **3~5 个可证伪假设**。
4. **定向插桩**验证假设。
5. 写**复现测试**，确认它红。
6. 修到绿，并**清理调试代码**。

第一个测试就是复现测试——保证修对了、且不回归。

## 交付判据（交回前自核）

1. **红 → 绿证据齐**：每个行为都有实现前的失败输出（含"为何预期失败"）+ 实现后的通过输出，且都是当场写的。
2. **收尾验证实跑绿**：完整跑过一次验证命令，看了退出状态与告警。
3. **没在红绿之间夹带重构**，看到的结构问题进了「疑虑」。

只有绿没有红 → 这个行为没走成 TDD：**重做一遍红 → 绿**（必要时 `git stash` 掉该行为的实现，
让测试真的红一次）；做不到就在记录里写明并告知调用方，**别事后补一段测试冒充红证据**。

## 卡住的升级阶梯

反复实现不过 / 需求歧义 / 外部依赖不可用 → 回读需求依据 → 拆更小的行为 → 交回 **BLOCKED** 说明卡在哪。
**不许什么都不变原样重试**，也不许硬猜往下冲。

## 反例（不要这样做）

❌ 不写测试直接糊实现，或先实现完再补一批测试然后声称做了 TDD —— 红没发生过，证据造不出来。
❌ 先把所有测试写完再写所有实现（水平切片）—— 验的是想象中的行为。
❌ 期望值用被测代码的同一套算法重算一遍（`toBe(items.reduce(...))`、按同样方式手推的快照）。
❌ mock 自己的模块 / 内部协作者、断言"某方法被调用过"、绕过被测接口直接查库验证。
❌ 在没约定过的 seam 上测（自己挑地方、去硬测 IO 外壳或私有实现）。
❌ 把重构和让测试转绿混在一步做 —— 结构问题记进疑虑。
❌ 红 → 绿边做边过，事后才凭印象把证据补写进记录 —— 追写出来的不是证据，是回忆。
❌ 记录里只写"测试都过了"，没有命令与输出。
❌ 一次性写完整功能、堆到几百行才跑一次测试。
❌ 自己 commit / 改 tasks checkbox / 动 git 状态 —— 那是调用方的闸门动作。
❌ 卡住了反复原样重试、空转，不交回 BLOCKED。

## 自检清单

- [ ] seam 是**约定过的**（来自设计 / 任务，或已与用户确认）？
- [ ] 一次一片（一个测试 → 一个最小实现 → 下一个），不是先写完所有测试？
- [ ] 每一轮的红都确认过失败原因是**行为缺失**，不是语法 / import / 环境错？
- [ ] 每转一次红 / 绿都**当场**写进了执行记录，不是事后追写、也不是只留在对话上下文里？
- [ ] 期望值来自独立字面量 / 规格，不是按代码算法重算？mock 只在系统边界？
- [ ] 让测试转绿的那一步没夹带重构，结构问题进了「疑虑」？
- [ ] bug 类改动先写了复现测试？小步推进、每步可构建可测？
- [ ] 收尾**完整跑了一次**验证命令（含 typecheck），输出进了记录？
- [ ] 没有 commit、没改 checkbox、没动 git 状态？
- [ ] 交回时说清了证据齐否、改动文件、疑虑、记录路径（或 BLOCKED 原因）？

## 相关参考

- [references/test-quality.md](references/test-quality.md) —— 好测试判据、三个反模式的识别信号与改法、
  mock 边界、期望值来源。
- [references/record-format.md](references/record-format.md) —— 执行记录的结构与写法（照抄填空）。
- `scripts/tdd-record.sh` —— 建工作目录 + 按标准结构建记录文件（已存在不覆盖），打印路径。

## 分层与调用方

- **composable 层**能力单元：通常由 workflow 层的 **execute-task**（阶段 2 每个任务）调用，
  也可被用户直接触发（"用 TDD 把这个改动做掉"）。
- 调用是**加载执行纪律，不是派 subagent**：调用方在自己的上下文里走完本闭环。
- seam 与验收标准来自上游 **make-design / split-task**；整体审查交 **review-changes**；
  提交、进度回写与收尾都归调用方。

