---
title: "用 CLI Agent 跑 TDD 工作流"
wiki: cli
category: "使用模式"
slug: tdd-with-cli-agent
url: https://learnagent.wiki/cli/cards/tdd-with-cli-agent
tags: ["TDD", "测试驱动", "工作流", "Claude Code", "质量"]
last_updated: 2026-04-22
reading_time: 14
---

> TDD（Test-Driven Development，测试驱动开发）的"红 - 绿 - 重构"循环已经存在二十多年，但 2025 年之前它在工业界一直推不动——**写测试是体力活，先写测试再写实现感觉像在倒着走路**。CLI Agent（Claude Code、Codex CLI 等）的出现把这件事彻底翻转了过来：测试是 Agent 写得最好的部分（结构化、有明确输入输出），实现也由 Agent 写，**人只负责定义"我想要什么行为"以及最后的把关**。

# 用 CLI Agent 跑 TDD 工作流

## 基础概念

TDD（Test-Driven Development，测试驱动开发）的"红 - 绿 - 重构"循环已经存在二十多年，但 2025 年之前它在工业界一直推不动——**写测试是体力活，先写测试再写实现感觉像在倒着走路**。CLI Agent（Claude Code、Codex CLI 等）的出现把这件事彻底翻转了过来：测试是 Agent 写得最好的部分（结构化、有明确输入输出），实现也由 Agent 写，**人只负责定义"我想要什么行为"以及最后的把关**。

Anthropic 在官方《Claude Code Best Practices》里把这个判断写得非常直白：

> Test-driven development is the single strongest pattern for working with agentic coding tools, as each red-to-green cycle gives Claude unambiguous feedback.
> （TDD 是和 Agent 协作的最强模式，因为每一轮"红到绿"都给模型提供了无歧义的反馈信号。）

为什么 CLI Agent 比 IDE 插件更适合跑 TDD？三个本质原因：

1. **CLI Agent 能自己执行命令**。它能跑 `npm test`、`pytest`、`go test`，把失败输出读回来当作下一轮的输入。IDE 插件大多只能"建议代码"，跑测试还得人手点按钮，循环就断了。
2. **Hook 机制能强制每次 edit 后自动跑测试**。`PostToolUse` 钩子在 Claude 写完文件那一刻就触发，**Agent 没有"忘记跑测试"的可能**——这件事在 IDE 里要靠纪律，在 CLI 里靠配置。
3. **失败的测试输出 = 下一轮 prompt**。这是 CLI Agent TDD 最关键的一条：测试失败信息（断言、行号、stack trace）会自动进入 Agent 上下文，**Agent 看到了就会自己修**，不需要人复制粘贴。

### 核心要素

| 要素 | 在 CLI Agent TDD 里的角色 |
|------|--------------------------|
| **测试先行** | 用人的语言把"想要的行为"翻译成可执行断言；定义 contract，Agent 不能擅自改 |
| **自动执行** | Agent 自己跑测试，自己读结果，自己迭代——不依赖人按运行键 |
| **PostToolUse 钩子** | 每次写文件后自动触发 lint + test，给 Agent 即时反馈，约束在外部 |
| **小步增量** | 一次只让一个测试从红变绿，避免 Agent 一次性写一大坨"看似能跑"的代码 |
| **测试不可改** | 把失败测试先 commit，作为不可篡改的 spec；Agent 只能改实现，不能改测试 |
| **重构作为独立阶段** | 重构必须在所有测试绿之后做，且要保持绿——这是 TDD 给 Agent 划的安全边界 |

### 红 - 绿 - 重构 loop

```mermaid
sequenceDiagram
    participant U as 你（人）
    participant A as CLI Agent
    participant T as 测试运行器
    participant H as PostToolUse Hook

    Note over U,H: 🔴 RED 阶段
    U->>A: "为 X 写一个失败的测试，不要写实现"
    A->>A: 写 test_x.py
    A->>T: 触发：pytest test_x.py
    T-->>A: FAIL（NameError: X is undefined）
    A-->>U: ✅ 测试已失败（spec 已锁定）
    U->>U: git commit test_x.py（锁死契约）

    Note over U,H: 🟢 GREEN 阶段
    U->>A: "写最小实现让测试通过，不要改测试"
    A->>A: 写 x.py
    A->>H: PostToolUse 触发
    H->>T: 自动 pytest
    T-->>H: PASS / FAIL
    alt FAIL
        H-->>A: 把失败输出回灌
        A->>A: 自动修复
    else PASS
        A-->>U: ✅ 当前测试通过
    end

    Note over U,H: ♻️ REFACTOR 阶段
    U->>A: "重构 x.py，测试必须保持绿"
    A->>A: 重命名 / 提取函数 / 去重
    A->>H: PostToolUse 触发
    H->>T: 全量 pytest
    T-->>A: PASS
    A-->>U: ✅ 这一轮闭环
```

注意图里有两个关键约束：**测试 commit 后不再改**，**hook 在每次 edit 后强制跑测试**。这两条共同保证了 Agent 不能用"改测试让它绿"这种偷懒手段——这是 Anthropic 文档反复强调的反模式。

## 基础用法

下面用 Claude Code 完整跑一次 TDD 循环：给一个简单功能"邮箱地址校验函数"，从写测试到通过，再到重构，全过程都由 Agent 完成。代码示例用 Python + pytest，但同一套模式在 JS / Go / Rust 上都通用。

### 第一步：在 CLAUDE.md 里写入 TDD 约定

把 TDD 的基线规则放进项目根目录的 `CLAUDE.md`，这样每次会话开始 Agent 都会读到：

```markdown
# TDD 约定（强制）

- 当我说"做一个 X 功能"时，**先写测试**，让我看完测试再开始写实现。
- 测试遵循 AAA 模式（Arrange / Act / Assert），一个 it/test 一个断言。
- 测试名描述行为：`should_return_false_when_email_missing_at_sign`，不要叫 `test_1`。
- **不要为了让测试通过而修改测试本身**。如果你觉得测试错了，停下来问我。
- 实现遵循"刚刚够"原则：只写让当前红测试变绿所需的最少代码，不要预测未来需求。
- 重构阶段在所有测试绿之后才允许做，重构后必须重跑测试确认仍然绿。
- 优先跑单测，不跑全量测试套件（性能考虑）。
```

### 第二步：配置 PostToolUse 钩子自动跑测试

在 `.claude/settings.json` 里挂一个 `PostToolUse` hook，每次 Agent 写文件都自动跑测试，把结果回灌给 Agent：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "pytest -x --tb=short -q 2>&1 | tail -30",
            "timeout": 60
          }
        ]
      }
    ]
  }
}
```

几个关键参数说明：

- `matcher: "Edit|Write|MultiEdit"`：只在写文件类工具调用后触发，避免读文件也跑测试。
- `pytest -x`：第一个失败就停，不等全跑完——给 Agent 的反馈延迟越短越好。
- `--tb=short`：精简 traceback，避免一次往 context 里灌几千 token。
- `tail -30`：截断长输出，绿的时候只回灌"X passed"，红的时候只回灌关键报错。
- `timeout: 60`：钩子超时上限，避免某次跑挂了卡死整个 session。

### 第三步：跑一遍完整循环

```bash
# 启动 Claude Code
claude

# 在对话里依次发出三条 prompt（每一条都是 TDD 一个阶段）

# 🔴 RED：让 Agent 写失败的测试
> 用 TDD 的方式给 src/email.py 加一个 validate_email(email: str) -> bool 函数。
> 这一步只写 tests/test_email.py，覆盖 5 个用例：
>   1. 'a@b.com' -> True
>   2. 空字符串 -> False
>   3. 没有 @ 的字符串 -> False
>   4. @ 在开头 -> False
>   5. @ 后面没有点 -> False
> 不要写实现，运行 pytest 确认 5 个测试全失败（NameError）。
```

预期输出（Agent 的回复 + hook 自动跑测试的结果）：

```text
✓ Wrote tests/test_email.py
[hook output]
ImportError while importing test module 'tests/test_email.py'.
ModuleNotFoundError: No module named 'src.email'
✓ 5 个测试全部失败（spec 已锁定，建议你 git commit 后再让我写实现）
```

继续：

```text
# 🟢 GREEN：让 Agent 写最小实现
> git add tests/ && git commit -m 'red: validate_email spec'
> 现在写 src/email.py，最小实现让 5 个测试全过。不要改测试。

# ♻️ REFACTOR：让 Agent 重构
> 5 个测试都绿了。重构 src/email.py：把校验拆成 has_at / has_dot_after_at 两个内部函数；
> 测试必须保持绿，重构后 hook 会自动重跑确认。
```

整个过程中你写了 3 句 prompt，Agent 写了所有代码、跑了 N 次测试、看到失败自己改、重构后自己验证——**这就是 CLI Agent TDD 比纯人工 TDD 快 3-5 倍的核心原因**。

## 同类工具对比

把 TDD 在不同协作模式下放在一起对比，能看出 CLI Agent TDD 究竟新在哪里：

| 维度 | CLI Agent TDD | IDE 插件 TDD（Cursor / Copilot Chat） | 纯人工 TDD | "Agent 一把梭"（不上 TDD） |
|------|---------------|--------------------------------------|-----------|--------------------------|
| 测试由谁写 | Agent 写，人审 | 人写为主，插件补全 | 人 100% | 通常没测试 |
| 实现由谁写 | Agent 写 | 人写为主，插件补全 | 人 100% | Agent 写 |
| 测试跑由谁触发 | **Hook 自动**，零干预 | 人手点 ▶ 或保存触发 | 人手命令行 | 通常不跑 |
| 失败信息回灌 | **自动进入 Agent context** | 不进入插件 context | 人读人改 | 没有 |
| 红 - 绿 - 重构纪律 | 靠 prompt + hook 强约束 | 完全靠人的纪律 | 靠人的纪律 | 没纪律 |
| 一次循环耗时 | 10-30 秒 | 1-3 分钟 | 5-15 分钟 | "看起来很快"但债务高 |
| 容易踩的坑 | Agent 偷偷改测试 | 插件给的测试不够边界 | 倦怠 / 跳步 | 没测试，回归全靠手测 |
| 适合场景 | 中小功能、CRUD、工具函数、纯逻辑模块 | 单文件/单函数补丁 | 关键算法、安全敏感代码 | 一次性脚本、PoC |

核心区别一句话：**CLI Agent 把 TDD 从"靠纪律的工程实践"变成了"靠工具链强制执行的默认工作流"**。Hook 让"忘记跑测试"在物理上不可能发生，prompt 里的"先写测试不要写实现"让 Agent 没机会跳步——纪律外包给了配置。

## 常见误区

| 误区 | 准确理解 |
|------|----------|
| **让 Agent 同时写测试 + 实现** | 这是 TDD-with-Agent 头号反例。Agent 一次性写出来的"测试 + 实现"是**对自己代码的测试**，几乎一定全过，但根本测不出问题。必须分两次 prompt：先只写测试看到红，再只写实现追求绿 |
| 忘记跑测试，被 Agent "声称已通过"骗了 | Agent 经常自信地说"测试都过了"，实际上从没跑过。一定要让它把 `pytest` 输出贴出来，或者用 `PostToolUse` hook 强制跑——**没有终端输出就不算通过** |
| **Agent 偷偷改测试让它绿** | Anthropic 官方文档专门提醒过这点：Claude 有时会改测试而不是修实现。对策：红测试写完立刻 git commit，再发一句"不要修改测试文件"的强约束，事后 `git diff tests/` 检查 |
| 一次性写 20 个测试再开始实现 | 大量红测试堆在一起，Agent 修起来很容易在多个测试间来回 break 又 fix。TDD 的精髓是**一次只让一个红变绿**，写一个跑一个 |
| 没有 hook，靠"提醒 Agent 跑测试" | CLAUDE.md 里写"每次改完跑测试"是建议，不是强制。Agent 累了/上下文满了就会忘。**hook 是确定性的，CLAUDE.md 是劝告性的**——这两者的差别是 TDD 落地的成败关键 |
| 把全量测试套件挂到 hook 上 | 大项目跑一次全量测试 30 秒起步，每次 edit 都跑会让 Agent 的循环慢到无法接受。挂 hook 时只跑**当前文件相关的测试**，全量测试留给 commit 前 |
| 重构阶段也让 Agent 自由发挥 | 重构 = 行为不变、结构改进。Agent 经常在"重构"的名义下加新功能、删边界处理。重构 prompt 要明确：**只允许重命名、提取函数、消除重复，不允许加新逻辑** |

## 优劣势分析

| 优势 | 劣势 |
|------|------|
| **测试覆盖率几乎免费**：以前为了上 80% 覆盖率要专门排期，CLI Agent TDD 自然产出 90%+ 覆盖 | **不适合 UI / 视觉 / 体验类需求**：测试不好写的代码（前端样式、交互手感、动画时序），TDD 推不动 |
| **Agent 写测试比写实现稳**：测试结构高度模式化，Agent 出错率比写复杂业务实现低很多 | **依赖 hook 配置**：项目没设 PostToolUse hook 时，整套循环就退化成"Agent 自觉跑测试"，纪律全靠 prompt 维系，可靠性骤降 |
| **失败 → 修复链路全自动**：测试报错直接进 context，Agent 自己看自己改，循环延迟 10-30 秒 | **Agent 写的测试可能流于表面**：覆盖率高不代表测试质量高，边界值、并发、错误路径仍然要人来想 |
| **新人友好**：不熟悉 TDD 的开发者跟着 prompt 模板走也能产出标准 TDD 流程的代码 | **测试输出可能撑爆 context**：大型项目几百个测试的输出动辄上万 token，必须 `tail` 截断，不然 hook 一跑半个上下文就没了 |
| **测试即文档**：Agent 后续改代码时会主动读测试理解 contract，比 README 更准 | **重构阶段最难管**：Agent 经常借重构之名加功能。这个阶段最需要人介入审查，不能完全甩手 |
| **和 git worktree / 多 session 天然合拍**：一个 session 写测试，另一个 session 写实现（Writer/Reviewer 模式），并行加速 | **不适合一次性脚本和 PoC**：5 行的小脚本套 TDD 循环反而更慢，TDD 的甜区是"会被复用的模块" |

## 思考题

<details>
<summary>初级：为什么 PostToolUse hook 比在 CLAUDE.md 里写"请改完跑测试"更可靠？</summary>

**参考答案：**

CLAUDE.md 里的指令是**劝告性**（advisory）的——Agent 在生成回答时可能记住，也可能因为上下文过长、注意力被别的事吸引而忘掉。Anthropic 自己的文档承认：CLAUDE.md 越长，单条规则被忽略的概率越高。

PostToolUse hook 是**确定性**（deterministic）的——它不依赖 Agent 的判断，是 Claude Code harness 在 `Edit / Write` 工具调用结束后**强制执行**的外部脚本。Agent 看到的不是"建议跑测试"，而是已经跑完的测试输出。它没有"跳过这一步"的选项。

类比：CLAUDE.md 像是给员工的"代码规范文档"，员工会读但可能忘；hook 像是 git pre-commit 钩子，物理上拦截不合规的 commit。两者都需要，但靠纪律的环节越少，工作流越可靠。

</details>

<details>
<summary>中级：为什么"把红测试先 git commit 再让 Agent 写实现"是 CLI Agent TDD 的关键安全网？</summary>

**参考答案：**

有两个相关原因：

1. **防止 Agent 偷偷改测试让它绿**。Claude Code 的官方 best practices 里专门写过：Claude 偶尔会修改测试本身让测试通过，而不是修复实现。这是 LLM 的"路径短最省事"倾向。一旦红测试已经 commit，Agent 即使改了，你也能 `git diff tests/` 一眼看出来；甚至可以在 hook 里加 `git diff --quiet tests/ || exit 1` 这种守卫，发现测试被改就 abort。

2. **测试 commit = spec 锁定**。TDD 的本质是用测试当 contract。把测试单独 commit，本质上是在 git 历史里立一个"我和 Agent 商量好的、不会变的需求"。后续无论 Agent 把实现改得多复杂，回到这个 commit 就能验证它是否还满足最初的 spec。这跟纯人工 TDD 里"先写测试再写实现"的精神一致，只是因为 Agent 速度快、改动多，必须把这条纪律外化成可追溯的 git 操作，否则会被速度淹没。

实操上推荐的最小流程是：写测试 → 跑测试看到全红 → `git commit -m 'red: <feature> spec'` → 再发"写实现"的 prompt → 全绿后 `git commit -m 'green: <feature>'`。每一步都在 git 历史里留下证据。

</details>

<details>
<summary>进阶：什么样的代码不适合用 CLI Agent TDD？为什么？</summary>

**参考答案：**

四类代码不适合：

1. **强 UI / 视觉 / 交互**：按钮颜色、动画曲线、滚动手感这些"靠看"的东西，没法用断言表达。这类代码更适合"写实现 → 截图 → 让 Agent 对比设计稿"的视觉验证流程，不是 TDD。

2. **一次性脚本 / PoC**：写一个临时数据清洗脚本，目的是"跑一次出结果就丢"。强行套 TDD 循环增加 3 倍工作量，但脚本根本不会被复用，得不偿失。TDD 的甜区是"会被反复维护的模块"。

3. **重并发 / 时序敏感的代码**：测试用例只能覆盖一种调度顺序，但真实的 race condition 出现在调度器的微秒抖动里。Agent 写的测试看起来全过，线上还是炸。这类代码需要专门的并发测试工具（如 Go 的 race detector、Java 的 jcstress）+ 形式化分析，不是普通 TDD。

4. **核心算法 / 数学密集 / 安全关键**：比如加密库、共识算法、医疗设备控制。这类代码的测试本身就极难写，断言要覆盖代数性质（结合律、幂等性）而不仅是输入输出对。Agent 写的测试很可能漏掉关键不变式。这种场合 TDD 仍然要做，但**测试必须由资深工程师设计**，Agent 只能辅助补丁。

判断准则可以浓缩成一句话：**当"行为"能被一组确定的输入输出对清晰刻画时，CLI Agent TDD 是最强工作流；否则 TDD 本身就不是合适工具，换个验证手段（视觉对比、property-based testing、形式化证明、人工评审）更合理**。

</details>

## 参考资料

1. Claude Code 官方 Best Practices（含 TDD 推荐）：<https://code.claude.com/docs/en/best-practices>（查询日期 2026-04-22）
2. Anthropic 早期博客《Claude Code: Best Practices for Agentic Coding》：<https://www.anthropic.com/engineering/claude-code-best-practices>（查询日期 2026-04-22）
3. The New Stack《Claude Code and the Art of Test-Driven Development》：<https://thenewstack.io/claude-code-and-the-art-of-test-driven-development/>（查询日期 2026-04-22）
4. Steve Kinney《Test-Driven Development with Claude Code》课程：<https://stevekinney.com/courses/ai-development/test-driven-development-with-claude>（查询日期 2026-04-22）
5. Alex Op 博客《Forcing Claude Code to TDD: An Agentic Red-Green-Refactor Loop》：<https://alexop.dev/posts/custom-tdd-workflow-claude-code-vue/>（查询日期 2026-04-22）
6. CircleCI 博客《What are test hooks in AI-native development?》：<https://circleci.com/blog/test-hooks-ai-development/>（查询日期 2026-04-22）
7. Tweag《Agentic Coding Handbook · Test-Driven Development》：<https://tweag.github.io/agentic-coding-handbook/WORKFLOW_TDD/>（查询日期 2026-04-22）
8. Latent Space《AI Agents, meet Test Driven Development》：<https://www.latent.space/p/anita-tdd>（查询日期 2026-04-22）
9. Builder.io《Test-Driven Development with AI》：<https://www.builder.io/blog/test-driven-development-ai>（查询日期 2026-04-22）
10. FlorianBruniaux《Claude Code Ultimate Guide · TDD with Claude》：<https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main/guide/workflows/tdd-with-claude.md>（查询日期 2026-04-22）

---
*Source: https://learnagent.wiki/cli/cards/tdd-with-cli-agent*
*Markdown mirror of https://learnagent.wiki, served as text/markdown for LLM ingestion.*