第 13 课:TDD 全流程 — RED-GREEN-IMPROVE

12 阅读8分钟

所属阶段:第三阶段「工作流实战」(第 12-20 课) 前置条件:第 12 课(调用链追踪) 本课收获:完整体验一次 TDD 循环,理解每个阶段的目的


一、本课概述

TDD(Test-Driven Development)不是 ECC 发明的,但 ECC 把它从"建议"变成了"强制"。在 ECC 的世界里,没有测试的代码 = 不存在的代码。

本课回答三个问题:

  1. TDD 在整个开发流程中处于什么位置? — 四阶段开发流
  2. TDD 的每个阶段到底做什么? — RED-GREEN-IMPROVE-VERIFY 详解
  3. 如何亲手执行一次完整的 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 必须修"
    ▼
阶段 4Commit & 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 中最反直觉但最重要的要求。理由有三:

  1. 验证测试本身有效 — 如果测试在实现之前就通过了,说明测试写错了(可能断言太宽松,或者测试的功能已经存在)
  2. 确认因果关系 — 只有先看到"失败",再看到"通过",你才能确信是你写的代码让测试通过的,而不是其他原因
  3. 明确需求 — 写不出失败测试 = 你还没想清楚要做什么(回到 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 时的边界处理
  • 空字符串输入
  • 非字符串输入

严格要求

  1. 先写所有测试,运行确认全部 FAIL
  2. 写最少代码让测试通过
  3. 重构后再跑一遍测试
  4. 检查覆盖率

练习 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。