# Test Driven Development

> 测试驱动开发纪律——实现功能、修复 bug 或改变行为前，沿已批准的公共测试落点先写失败测试，确认有效红后最小实现转绿。批准的前置或收尾纯重构用行为测试保绿。例外按单点清单与可追溯授权处理。

- Skill: `flamemida/test-driven-development` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add flamemida/test-driven-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/flamemida/test-driven-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: flamemida (https://skillmd.com/u/flamemida)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/flamemida/test-driven-development

---


> 语言协议：以对话语言输出——用户显式指定（含平台 `language` 设置）优先，其次跟随用户近期消息语言；均无法判定时默认英语。落盘产物以创建时对话语言为准，增量修改保持产物既有语言。本 skill 中的固定话术是语义模板，用对话语言表达其意，不逐字照搬。

> **外部搜索统一入口**：需要联网检索（资料、库/框架文档、时效信息）时一律先用 anysearch skill（插件内嵌），不可用再降级 WebSearch/WebFetch；降级链与派发词要求见 requirement-analysis 的 references/exploration-patterns.md。

# 测试驱动开发（TDD）

## 概述

先写测试。看它失败。写最小代码让它通过。

**核心原则**：没看着测试失败过，你就不知道它测的是不是对的东西。

本 skill 只依赖项目自身的测试命令，不依赖任何平台专属工具，Claude Code 与 Codex 通用。

**违反规则的字面 = 违反规则的精神。**

## 何时使用

**总是**：新功能、bug 修复、行为变更走红绿循环；纯重构走本文「收尾纯重构」，不伪造红。

**例外清单（单点定义）**：一次性原型、生成代码、配置文件，以及不改变行为、契约或测试预期的纯文案。例外需要可追溯的用户授权；同一范围内已授权则引用原决定，不反复询问。技能规则、API 语义与测试预期即使写在 Markdown 中，也不是纯措辞。

想着"就这一次跳过 TDD"？停。那是合理化借口。

## 测试落点确认与消费

测试落点（seam）是观察公共行为的契约边界，可为模块函数、CLI、HTTP API、数据库适配器，不限于最外层界面。优先复用已有落点，选择仍能稳定观察目标行为的较高层接口，尽量少建新接口；一个落点可覆盖多个 Scenario，不逐私有函数制造测试。

spec/plan 已批准的落点直接使用，不重复确认。存量无 seam 标签但批准的精确接口和 Scenario 唯一指向同一边界时，只提取已有决定并记录来源，不迁移旧计划格式或推测新决定。

没有唯一落点或 spec/plan 冲突时，停止受影响任务的测试和实现，主线程走原澄清/偏差流程，不默默择一覆盖。即兴任务在写测试前确认公共接口和落点，quick-fix 并入已有修复确认环节，一次一题。implementer 回报 blocked，不向用户另开门、不改 plan 或扩大 writes。

## 铁律

```
没有有效失败测试，就不写新增或改变行为的生产代码
```

未按纪律先写了行为实现？删除本次未经测试先行的实现，从失败测试重做，不留作参考；不删除无关既有工作。纯重构与已授权例外按对应规则处理，不冒充新增行为的红证据。

## 红绿循环

```dot
digraph tdd_cycle {
    rankdir=LR;
    red [label="红\n写失败测试", shape=box, style=filled, fillcolor="#ffcccc"];
    verify_red [label="确认失败\n原因正确", shape=diamond];
    green [label="绿\n最小实现", shape=box, style=filled, fillcolor="#ccffcc"];
    verify_green [label="确认通过\n全绿", shape=diamond];
    next [label="下一个", shape=ellipse];

    red -> verify_red;
    verify_red -> green [label="是"];
    verify_red -> red [label="失败原因\n不对"];
    green -> verify_green;
    verify_green -> green [label="否"];
    verify_green -> next [label="是"];
    next -> red;
}
```

### 红——写失败测试

在获批 seam 写一个最小测试：一个公共行为、清晰命名、独立真值。mock 准入遵循 references/testing-anti-patterns.md，不用自己的算法重算期望值。

```typescript
// ✅ 好：名字表意、测真实行为、只测一件事
test('retries failed operations 3 times', async () => {
  let attempts = 0;
  const operation = () => {
    attempts++;
    if (attempts < 3) throw new Error('fail');
    return 'success';
  };
  const result = await retryOperation(operation);
  expect(result).toBe('success');
  expect(attempts).toBe(3);
});

// ❌ 坏：名字含糊、测的是 mock 不是代码
test('retry works', async () => {
  const mock = jest.fn()
    .mockRejectedValueOnce(new Error())
    .mockRejectedValueOnce(new Error())
    .mockResolvedValueOnce('success');
  await retryOperation(mock);
  expect(mock).toHaveBeenCalledTimes(3);
});
```

### 确认红——看它失败

**行为红绿路径的强制步骤。** 确认失败源于目标行为缺失，而非拼写、编译或环境故障；刻画已有行为遵循「收尾纯重构」，不冒充红证据。

- **行为缺陷测试直接通过了？** 核实是否复现目标缺陷并修正无效测试。纯重构刻画应先观察当前行为通过，不适用本条。
- **测试报错了？** 修到它以正确原因失败为止。

### 绿——最小实现

写让测试通过的最简代码。不加多余功能、不顺手重构别的代码、不超出测试要求"改进"。

```typescript
// ✅ 好：刚好够通过
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
  for (let i = 0; i < 3; i++) {
    try { return await fn(); } catch (e) { if (i === 2) throw e; }
  }
  throw new Error('unreachable');
}

// ❌ 坏：maxRetries/backoff/onRetry 没人要 —— YAGNI
```

### 确认绿——看它通过

**强制步骤。** 运行测试，确认：目标测试通过、其他测试仍通过、输出干净（无报错无警告）。

- **公共行为回归？** 修实现。已核实行为未变但测试绑定私有细节时，按反模式 7 修正落点，不迁就脆弱断言。
- **其他测试挂了？** 现在就修。

### 重构候选

发现重复、命名或结构清理机会时，记录位置、理由与保护证据，交既有收尾，不插入每次红绿循环。每票第五步仍为提交；契约自检只查 over/under-building 与接口锚定。

### 重复

为下一个行为写下一个失败测试。

## 批准的前置纯重构与集成组

批准计划中解除实际实施阻碍的前置纯重构在 T01 执行，复用下方前后公共行为保护规则；不能等到收尾才解除阻碍。执行中额外发现的重构机会仍交收尾，不借此前置规则扩大授权。

集成组不是 TDD 例外：行为变化在实现前仍获得有效行为失败；纯机械/行为保持迁移先取得组基线保护。每票照常跑能运行的检查，仅对计划逐项声明的命令/范围/原因延后整体绿，保存真实 exit 与待验状态。意外局部失败阻塞，环境/编译错误不能当行为红。没有足够保护或无法获得行为红时回到设计，不把该票改名“重构”来跳过。

组员的公共行为最终在唯一组验证票上检验，普通功能票仍保持五步和有效红绿。没有有效红的纯重构与组员/组验证票不派给普通 implementer，不改变 implementation-result phase 或原接收条件。组验证完成不替代最终特性审查、相关回归和收尾全量。

## 收尾纯重构

先核实授权范围及公共行为不变，再用已有行为测试记录调整前后的通过证据；缺保护时，先依据现行契约补行为刻画并观察当前实现通过，再结构调整。刻画通过不是红证据；若当前行为与需求不同，转缺陷复现红绿路径。

计划中的前置纯重构与组迁移按已批准时机执行，额外候选由现有收尾审查接收，收尾修复后复审受影响维度；quick-fix 使用自身收尾，不强制开启可选 acceptance-qa。处置沿已有授权边界。implementer 无法提供有效红证据的纯重构票回报 blocked，由主线程按现有串行分流处理，不改结果 schema 或普通实施票的红绿条件。

## 为什么顺序重要（借口对照表）

| 借口 | 现实 |
|------|------|
| "太简单不用测" | 简单代码也会坏。测试 30 秒的事。 |
| "我写完再补测试" | 事后测试立即通过，什么都证明不了——可能测错对象、测实现不测行为、漏掉你忘了的边缘情况。 |
| "事后测试达到同样目的" | 事后测试回答"这代码做了什么"；事前测试回答"这代码该做什么"。事后测试被实现偏置——你测你建的，不是需求要的。 |
| "我已经手工测过了" | 手工测试无记录、不可重跑、高压下必漏。"我试了没问题" ≠ 系统性验证。 |
| "删掉 X 小时的工作太浪费" | 沉没成本谬误。留着没有真实测试的代码才是技术债。 |
| "留着当参考，先写测试" | 你会照着改。那就是事后测试。删除就是删除。 |
| "需要先探索" | 可以。探索完扔掉，从 TDD 重新开始。 |
| "测试难写 = 该 mock 更多" | 测试难写 = 设计有问题。难测即难用。 |
| "TDD 教条，我要务实" | TDD 就是务实：提交前抓 bug 比事后调试快，回归即刻暴露，测试即文档，重构有保护网。"务实的捷径" = 在生产环境调试 = 更慢。 |

## Red Flags —— 停下来重来

- 新增/改变行为的代码先于有效失败测试；缺陷测试未复现就立即通过
- 说不清测试为什么失败
- "就这一次"、"我已经手工测过了"、"事后测试目的一样"、"重要的是精神不是仪式"
- "留作参考"、"改造现有代码"、"已经花了 X 小时删了可惜"
- "TDD 教条，我在务实"、"这次情况特殊因为……"

**核实违反红绿纪律后，删除本次违规行为实现并重做。** 纯重构刻画和已授权例外不误判为违规。

## 完成前检查清单

- [ ] 获批 seam 的公共行为、相关 Scenario、边缘与错误路径已有覆盖，不按新增函数数量要求直测
- [ ] 新增/改变行为的测试观察到有效红；纯重构另有前后绿，刻画不冒充红
- [ ] 红证据源于行为缺失，编译/环境故障、零测试、SKIP 不当 PASS
- [ ] 行为红绿使用最小实现；纯重构保持已有契约
- [ ] 目标及相关测试通过，计划最终全量和范围外失败裁决保持
- [ ] 输出干净（无报错、无警告）
- [ ] 测试验证真实行为，mock 遵循 testing-anti-patterns 的准入与声明依赖边界
- [ ] 边缘情况与错误路径已覆盖

按适用路径核对，不把其他路径的证据冒充自己的完成依据；例外有授权来源，重构候选已交收尾。

## 调试集成

发现 bug？先写复现它的失败测试，再走 TDD 循环。测试既证明修复又防止回归。**永远不要不带测试修 bug。**

## Mock 与测试反模式

添加 mock、测试工具方法、或想给生产类加测试专用方法时，先读 [testing-anti-patterns.md](references/testing-anti-patterns.md)：不测 mock 的行为、不给生产类加测试专用方法、不在不理解依赖链时 mock、mock 必须镜像真实结构的完整字段。

## 最终规则

```
新增或改变行为 → 有效红 → 最小实现 → 绿
纯重构 → 行为保护 → 结构调整 → 保持绿
例外 → 唯一清单及可追溯授权；普通实施票的红绿条件不变
```

未经用户允许，没有例外。

