所属阶段:第三阶段「工作流实战」(第 12-20 课) 前置条件:第 12 课(调用链追踪) 本课收获:完整体验一次 TDD 循环,理解每个阶段的目的
一、本课概述
TDD(Test-Driven Development)不是 ECC 发明的,但 ECC 把它从"建议"变成了"强制"。在 ECC 的世界里,没有测试的代码 = 不存在的代码。
本课回答三个问题:
- TDD 在整个开发流程中处于什么位置? — 四阶段开发流
- TDD 的每个阶段到底做什么? — RED-GREEN-IMPROVE-VERIFY 详解
- 如何亲手执行一次完整的 TDD 循环? — 从零写一个
slugify函数
学完本课,你将能独立完成一次严格的 TDD 循环,包括测试先行、最小实现、重构优化和覆盖率验证。
二、四阶段开发流
在 ECC 的 development-workflow.md 中定义了完整的功能开发流程。TDD 不是孤立的,它是四阶段开发流中的第二步:
阶段 0:Research & Reuse(研究与复用)
│ GitHub 搜索、库文档、包注册表
│ "找到能解决 80%+ 问题的现有实现"
▼
阶段 1:Plan(规划) ← planner Agent
│ 需求分析 → 架构设计 → 步骤拆解
│ "必须等用户确认后才动手"
▼
阶段 2:TDD(测试驱动开发) ← tdd-guide Agent
│ RED → GREEN → IMPROVE → VERIFY
│ "测试先行,80%+ 覆盖率"
▼
阶段 3:Review(代码审查) ← code-reviewer Agent
│ 安全检查 → 质量检查 → 分级报告
│ "CRITICAL 和 HIGH 必须修"
▼
阶段 4:Commit & Push(提交)
│ Conventional Commits 格式
│ CI/CD 通过后才能合并
▼
阶段 5:Pre-Review Checks(合并前检查)
│ 自动检查通过 → 解决冲突 → 请求 Review
关键洞察:TDD 之前必须有 Plan,TDD 之后必须有 Review。跳过任何一步都会导致后续步骤的质量下降。
三、TDD 严格循环详解
3.1 四个阶段
ECC 的 TDD 循环比传统的 RED-GREEN-REFACTOR 多了一步 VERIFY:
┌─────────────────────────────────────────────────┐
│ TDD 严格循环 │
│ │
│ ┌──────┐ ┌──────┐ ┌──────────┐ ┌──────┐│
│ │ RED │ → │GREEN │ → │ IMPROVE │ → │VERIFY││
│ │写测试│ │最小实现│ │重构优化 │ │覆盖率 ││
│ │必须失败│ │刚好通过│ │测试仍绿 │ │80%+ ││
│ └──────┘ └──────┘ └──────────┘ └──────┘│
│ ▲ │ │
│ └─────── 下一个功能点 ◀───────────────┘ │
└─────────────────────────────────────────────────┘
3.2 RED 阶段 — 写测试,必须失败
做什么:写一个描述期望行为的测试,然后运行它,确认它失败。
为什么测试必须先失败?
这是 TDD 中最反直觉但最重要的要求。理由有三:
- 验证测试本身有效 — 如果测试在实现之前就通过了,说明测试写错了(可能断言太宽松,或者测试的功能已经存在)
- 确认因果关系 — 只有先看到"失败",再看到"通过",你才能确信是你写的代码让测试通过的,而不是其他原因
- 明确需求 — 写不出失败测试 = 你还没想清楚要做什么(回到 Plan 阶段)
RED 阶段 = 需求定义阶段
测试写得出来 → 需求明确 → 继续
测试写不出来 → 需求不清 → 回到 Plan
3.3 GREEN 阶段 — 最小实现,刚好通过
做什么:写最少的代码让测试通过。不多写一行。
为什么是"最少"的代码?
- 避免过度工程化 — 只实现测试要求的行为,不猜测未来需求(YAGNI)
- 保持每步可验证 — 小步前进,每步都有测试保驾护航
- 为重构留空间 — GREEN 阶段的代码可以很丑,重构在 IMPROVE 阶段做
常见错误:GREEN 阶段就开始优化代码结构。这是错误的 — 先让测试通过,再优化。
3.4 IMPROVE 阶段 — 重构优化,测试仍绿
做什么:在测试通过的保护下,改善代码质量。每次修改后运行测试,确保测试仍然通过。
可以做的事:
- 提取常量(消除 Magic Numbers)
- 重命名变量(提高可读性)
- 提取辅助函数(降低复杂度)
- 消除重复代码(DRY)
- 改用不可变模式(Immutability)
铁律:重构期间测试必须保持绿色。如果测试变红了,说明重构改变了行为 — 回退并重来。
3.5 VERIFY 阶段 — 覆盖率检查
做什么:运行覆盖率工具,确保达到 80% 以上的覆盖率。
# Node.js 项目
npm test -- --coverage
# 覆盖率要求
# 常规代码:80%+
# 关键代码(金融计算、认证逻辑、安全相关):100%
四、AAA 模式(Arrange-Act-Assert)
每个测试用例都应该遵循 AAA 模式。这是 ECC testing.md 中明确推荐的结构:
test('returns empty array when no markets match query', () => {
// Arrange — 准备测试数据和环境
const markets = [
{ name: 'BTC-USD', volume: 1000 },
{ name: 'ETH-USD', volume: 500 },
];
const query = 'DOGE';
// Act — 执行被测行为
const result = filterMarkets(markets, query);
// Assert — 验证结果
expect(result).toEqual([]);
});
4.1 三个阶段的职责
| 阶段 | 职责 | 常见错误 |
|---|---|---|
| Arrange | 准备输入数据、Mock 依赖、设置初始状态 | 使用过于复杂的 Mock |
| Act | 调用被测函数/方法,仅此一步 | 在 Act 中做多个操作 |
| Assert | 验证输出、副作用、异常 | 断言太宽松(如只检查非空) |
4.2 测试命名规范
ECC 推荐使用描述行为的测试名称:
// GOOD — 描述被测行为和条件
test('returns empty array when no markets match query', () => {});
test('throws error when API key is missing', () => {});
test('falls back to substring search when Redis is unavailable', () => {});
// BAD — 描述实现细节
test('test filterMarkets function', () => {});
test('test error', () => {});
test('test Redis fallback', () => {});
命名公式:{行为} when {条件}
五、实战:slugify 函数的 TDD 循环
下面我们完整走一遍 TDD 循环,实现一个 slugify 函数(将文本转为 URL 友好的格式)。
5.1 RED — 写测试,确认失败
// slugify.test.js
const { slugify } = require('./slugify');
describe('slugify', () => {
test('converts spaces to hyphens', () => {
// Arrange
const input = 'hello world';
// Act
const result = slugify(input);
// Assert
expect(result).toBe('hello-world');
});
test('converts to lowercase', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('removes special characters', () => {
expect(slugify('hello! @world#')).toBe('hello-world');
});
test('collapses consecutive hyphens', () => {
expect(slugify('hello world')).toBe('hello-world');
});
test('trims leading and trailing hyphens', () => {
expect(slugify(' hello world ')).toBe('hello-world');
});
test('returns empty string for empty input', () => {
expect(slugify('')).toBe('');
});
});
运行测试:
node --test slugify.test.js
# 预期输出:6 个测试全部 FAIL
# Error: Cannot find module './slugify'
确认失败。测试本身是有效的 — 它在期望一个尚不存在的模块。
5.2 GREEN — 最小实现
// slugify.js
function slugify(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
module.exports = { slugify };
运行测试:
node --test slugify.test.js
# 预期输出:6 个测试全部 PASS
确认通过。注意这里的实现是"刚好够用"的 — 没有处理 Unicode、没有做参数校验、没有导出类型定义。
5.3 IMPROVE — 重构优化
// slugify.js — 重构后
const PATTERNS = {
NON_ALPHANUMERIC: /[^a-z0-9\s-]/g,
WHITESPACE: /\s+/g,
CONSECUTIVE_HYPHENS: /-+/g,
LEADING_TRAILING_HYPHENS: /^-|-$/g,
};
function slugify(text) {
if (typeof text !== 'string') {
throw new TypeError('slugify expects a string argument');
}
return text
.toLowerCase()
.replace(PATTERNS.NON_ALPHANUMERIC, '')
.replace(PATTERNS.WHITESPACE, '-')
.replace(PATTERNS.CONSECUTIVE_HYPHENS, '-')
.replace(PATTERNS.LEADING_TRAILING_HYPHENS, '');
}
module.exports = { slugify };
改进了什么:
- 提取正则为命名常量(消除 Magic Patterns)
- 添加参数类型检查(输入验证)
- 保持不可变 — 每个
replace返回新字符串,不修改原始值
运行测试确认仍然通过:
node --test slugify.test.js
# 预期输出:6 个测试仍然全部 PASS
此时可以补充一个类型检查的测试:
test('throws TypeError for non-string input', () => {
expect(() => slugify(123)).toThrow(TypeError);
expect(() => slugify(null)).toThrow(TypeError);
});
5.4 VERIFY — 覆盖率检查
npx c8 node --test slugify.test.js
# 预期输出:
# File | % Stmts | % Branch | % Funcs | % Lines
# ------------|---------|----------|---------|--------
# slugify.js | 100 | 100 | 100 | 100
#
# Coverage: 100% PASS (Target: 80%)
TDD 循环完成。
六、development-workflow.md 的完整流程
把 TDD 循环放回完整开发流程的上下文中:
0. Research & Reuse
│ ├─ GitHub 搜索:gh search repos / gh search code
│ ├─ 库文档:Context7 / 官方文档
│ ├─ Exa:前两步不够时的补充
│ └─ 包注册表:npm / PyPI / crates.io
▼
1. Plan(planner Agent, Opus 模型)
│ ├─ 需求分析和重述
│ ├─ 架构变更清单
│ ├─ 分阶段实施步骤
│ └─ 等待用户确认
▼
2. TDD(tdd-guide Agent, Sonnet 模型)
│ ├─ RED:写失败测试
│ ├─ GREEN:最小实现
│ ├─ IMPROVE:重构
│ └─ VERIFY:80%+ 覆盖率
▼
3. Review(code-reviewer Agent, Sonnet 模型)
│ ├─ CRITICAL / HIGH / MEDIUM / LOW 分级
│ └─ 修复 CRITICAL 和 HIGH
▼
4. Commit
│ ├─ Conventional Commits 格式
│ └─ feat / fix / refactor / docs / test / chore / perf / ci
▼
5. Pre-Review Checks
│ ├─ CI/CD 通过
│ ├─ 冲突解决
│ └─ 分支同步
七、TDD 反模式
ECC 的 tdd-guide.md 明确列出了必须避免的反模式:
| 反模式 | 为什么有害 |
|---|---|
| 先写实现再补测试 | 测试变成了"确认现有行为"而非"定义期望行为" |
| 跳过 RED 阶段 | 无法确认测试本身是有效的 |
| GREEN 阶段写太多代码 | 引入未经测试的行为,违反 YAGNI |
| 测试实现细节而非行为 | 重构时测试就碎了,维护成本极高 |
| 测试之间共享状态 | 一个测试的失败会连锁影响其他测试 |
| Mock 所有东西 | 测试只验证了 Mock 的行为,不验证真实行为 |
| 断言太少 | 测试通过但实际没验证任何有意义的事情 |
八、本课练习
练习 1:完整 TDD 循环(30 分钟)
严格按照 RED → GREEN → IMPROVE → VERIFY 的顺序,为以下函数完成一次 TDD 循环:
函数需求:truncate(text, maxLength) — 截断文本到指定长度,超过时添加 "..."
测试用例提示:
- 短于 maxLength 时原样返回
- 等于 maxLength 时原样返回
- 长于 maxLength 时截断并加 "..."
- maxLength 小于 3 时的边界处理
- 空字符串输入
- 非字符串输入
严格要求:
- 先写所有测试,运行确认全部 FAIL
- 写最少代码让测试通过
- 重构后再跑一遍测试
- 检查覆盖率
练习 2:识别反模式(10 分钟)
以下代码有什么 TDD 反模式?列出所有问题:
test('test the processOrder function', () => {
const order = processOrder({ items: [{ id: 1, price: 10 }] });
expect(order).toBeTruthy();
});
练习 3(选做):阅读 tdd-guide Agent
打开 agents/tdd-guide.md,回答:
- 它的 model 是什么?为什么不用 Opus?
- 它必须测试哪 8 种边界情况?
- 它的质量检查清单有几项?
九、本课小结
| 你应该记住的 | 内容 |
|---|---|
| 四阶段开发流 | Research → Plan → TDD → Review → Commit |
| TDD 四步 | RED(写失败测试)→ GREEN(最小实现)→ IMPROVE(重构)→ VERIFY(80%+) |
| RED 阶段意义 | 验证测试有效、确认因果、明确需求 |
| AAA 模式 | Arrange(准备)→ Act(执行)→ Assert(断言) |
| 测试命名 | {行为} when {条件} |
| 覆盖率要求 | 常规 80%+,关键代码 100% |
十、下节预告
第 14 课:验证循环 — 从代码到可提交
TDD 只是验证的第一步。下节课我们将学习完整的验证循环:测试通过 → Lint 通过 → 类型检查通过 → 安全检查通过 → 才能提交。任何一步失败都要从头重跑。你会用第 13 课写的代码完整走一遍这个流程。
预习建议:阅读 rules/common/coding-style.md 的 Code Quality Checklist 和 rules/common/security.md 的 Mandatory Security Checks。