mcpField
T
test-driven-development

test-driven-development

github.com/romiluz13测试与质量16636 次安装1 周前更新
代码仓库

关于 test-driven-development

面向软件开发任务的 TDD 技能,要求先编写并观察测试失败,再实现最小改动并重构。它通过逐项循环、运行模式纪律和验证清单,规范测试优先的开发过程。

主要功能

  1. 01遵循 Red-Green-Refactor 循环
  2. 02每次处理一个行为测试
  3. 03要求确认测试先失败再通过
  4. 04规范测试运行模式与进程清理
  5. 05提供测试设计和模拟指南
  6. 06包含覆盖率与完成检查清单

适用场景

  1. 01为新功能先建立行为测试
  2. 02修复缺陷并验证回归
  3. 03重构代码时逐步保持测试通过

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/romiluz13/cc10x --skill test-driven-development

安装后在 Claude Code 中输入/test-driven-development确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
integration-and-live-proof.md
test-data-and-mocks.md
testing-patterns.md

详细介绍

测试驱动开发(TDD)

概述

先写测试。观察它失败。编写最少的代码使其通过。

核心原则: 如果你没有亲眼看到测试失败,就不知道它是否测试了正确的内容。

违反规则的字面要求,就是违背规则的精神。

参考文件

只阅读当前测试周期所需的参考资料:

  • references/testing-patterns.md,了解命名、AAA 结构、近似边界的负面测试、行为关注点和反模式检查
  • references/test-data-and-mocks.md,了解工厂函数、模拟边界、常见边界模拟,以及环境变量和时间的处理
  • references/integration-and-live-proof.md,当单元测试不足,或计划要求使用真实 API、种子数据、浏览器流程或压力验证时阅读

何时使用

始终适用:

  • 新功能
  • Bug 修复
  • 重构
  • 行为变更

例外情况(请咨询你的人工搭档):

  • 一次性原型
  • 生成的代码
  • 配置文件

想“这次就跳过 TDD”?停下。这是在为自己找借口。

铁律

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

先写代码再写测试?删掉它。从头开始。

没有例外:

  • 不要把它留作“参考”
  • 写测试时不要“改编”它
  • 不要看它
  • 删除就是彻底删除

根据测试重新实现。就这样。

测试流程纪律(关键)

问题: 测试运行器(Vitest、Jest)默认启用监听模式,导致进程无限期挂起。

强制规则:

  1. 始终使用运行模式——绝不调用监听模式:
    • Vitest:npx vitest run(不要用 npx vitest)
    • Jest:CI=true npx jest 或 npx jest --watchAll=false
    • npm 脚本:CI=true npm test 或 npm test -- --run
  2. 所有测试命令优先使用 CI=true 前缀:CI=true npm test
  3. TDD 周期完成后,确认没有孤立进程: pgrep -f "vitest|jest" || echo "Clean"
  4. 如发现进程,则将其终止:pkill -f "vitest" 2>/dev/null || true

红—绿—重构

    ┌─────────┐       ┌─────────┐       ┌───────────┐
    │   RED   │──────>│  GREEN  │──────>│ REFACTOR  │
    │ (Fail)  │       │ (Pass)  │       │ (Clean)   │
    └─────────┘       └─────────┘       └───────────┘
         ^                                    │
         │                                    │
         └────────────────────────────────────┘
                    Next Feature
垂直切片(关键)
WRONG (horizontal — all tests then all code):
  RED:   test1, test2, test3, test4, test5
  GREEN: impl1, impl2, impl3, impl4, impl5

RIGHT (vertical — one feature at a time):
  RED->GREEN: test1->impl1
  RED->GREEN: test2->impl2
  RED->GREEN: test3->impl3

不要先写完所有测试,再写完所有实现。 这样会产生糟糕的测试:

  • 批量编写的测试验证的是_想象中的_行为,而不是_实际的_行为
  • 最终测试的是事物的_形式_,而不是面向用户的行为
  • 测试会对真实变化失去敏感性——行为出问题时仍会通过,行为正常时反而失败
  • 你会超前于实际进展,在理解实现之前就确定测试结构

正确做法: 一个测试 → 一个实现 → 重复。每个测试都应回应你从上一个周期中学到的内容。

RED - 编写失败的测试

编写一个最精简的测试,展示应该发生什么。

良好示例:

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);
});

名称清晰,测试真实行为,只测试一件事

不良示例:

test('retry works', async () => {
  const mock = jest.fn()
    .mockRejectedValueOnce(new Error())
    .mockRejectedValueOnce(new Error())
    .mockResolvedValueOnce('success');
  await retryOperation(mock);
  expect(mock).toHaveBeenCalledTimes(3);
});

名称含糊,测试的是模拟对象而非代码

要求:

  • 一个行为
  • 名称清晰
  • 真实代码(除非无法避免,否则不使用模拟)
验证 RED - 观察它失败

必须执行。绝不能跳过。

CI=true npm test path/to/test.test.ts

确认:

  • 测试失败(不是报错)
  • 失败信息符合预期
  • 失败原因是功能缺失(而非拼写错误)

测试通过了? 你测试的是已有行为。修正测试。

测试报错了? 修复错误,重新运行,直到它正确地失败。

GREEN - 编写最少的代码

编写能让测试通过的最简单代码。

良好示例:

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');
}

只写足以通过测试的代码

不良示例:

async function retryOperation<T>(
  fn: () => Promise<T>,
  options?: {
    maxRetries?: number;
    backoff?: 'linear' | 'exponential';
    onRetry?: (attempt: number) => void;
  }
): Promise<T> {
  // YAGNI - You Ain't Gonna Need It
}

过度设计

不要添加功能、重构其他代码,或超出测试范围进行“改进”。不要硬编码测试值——应实现适用于_所有_输入的通用逻辑。

验证 GREEN - 观察它通过

必须执行。

CI=true npm test path/to/test.test.ts

确认:

  • 测试通过
  • 其他测试仍然通过
  • 输出干净(没有错误或警告)

测试失败? 修复代码,不要改测试。

其他测试失败? 立即修复。

REFACTOR - 清理

只有通过后才能进行:

  • 消除重复
  • 改进命名
  • 提取辅助函数

保持测试通过。不要添加行为。

重复

为下一个功能编写下一个失败的测试。

优秀测试

质量好差
精简聚焦一件事。名称里有「和」?拆分它。test('validates email and domain and whitespace')
清晰名称描述行为test('test1')
体现意图展示期望的 API让代码应该做什么变得模糊

如需深入了解测试结构、容易混淆的反向测试、行为与内部实现的区别,以及异味检查,请阅读 references/testing-patterns.md。

有关工厂、模拟对象以及环境变量和时间的处理,请阅读 references/test-data-and-mocks.md。

为什么顺序很重要

「我会在之后编写测试来验证它是否可用」

在代码之后编写的测试会立即通过。立即通过并不能证明任何事:

  • 可能测试了错误的内容
  • 可能测试的是实现,而非行为
  • 可能遗漏了你忘记的边缘情况
  • 你从未见过测试捕获这个错误

先编写测试,迫使你看到测试失败,从而证明它确实在测试某些内容。

「我已经手动测试过所有边缘情况了」

手动测试是临时性的。你以为自己测了所有情况,但:

  • 没有测试记录
  • 代码变更后无法重新运行
  • 赶时间时很容易忘记某些情况
  • 「我试过了,能用」≠ 全面测试

自动化测试是系统性的。每次运行的方式都相同。

「删除 X 小时的工作太浪费了」

这就是沉没成本谬误。时间已经花出去了。现在你有两个选择:

  • 删除并用 TDD 重写(再花 X 小时,信心更足)
  • 保留代码,之后再补测试(30 分钟,信心不足,还可能有漏洞)

真正的「浪费」是保留你无法信任的代码。没有真正测试的可用代码就是技术债务。

危险信号——停止并重新开始

如果你发现自己:

  • 先写代码再写测试
  • 实现后再写测试
  • 测试立即通过
  • 无法解释测试为什么失败
  • 「之后」再添加测试
  • 为「就这一次」找借口
  • 「我已经手动测试过了」
  • 「之后再写测试也能达到相同目的」
  • 「重在精神,不在形式」
  • 「保留作参考」或「改编现有代码」
  • 「已经花了 X 小时,删除太浪费了」
  • 「TDD 太教条了,我是在务实行事」
  • 「这次情况不同,因为……」

以上任何一种情况都意味着:删除代码。用 TDD 重新开始。

防止自我辩解

借口事实
「太简单,不值得测试」简单代码也会出错。测试只需 30 秒。
「我之后再测」测试立即通过并不能证明任何事。
「之后再写测试也能达到相同目的」先写测试 =「这应该做什么?」之后写测试 =「这做了什么?」
「已经手动测试过了」临时测试 ≠ 系统测试。没有记录,也无法重新运行。
「删除 X 小时的工作太浪费了」沉没成本谬误。保留未经验证的代码就是技术债务。
「保留作参考,先写测试」你会去改编它。这就是事后测试。删除就是删除。
「我得先探索一下」没问题。丢弃探索代码,从 TDD 开始。
「测试很难 = 设计不清晰」听听测试的反馈。难以测试 = 难以使用。
「TDD 会拖慢我」TDD 比调试更快。务实的做法 = 先写测试。
「手动测试更快」手动测试无法证明边缘情况没问题。每次变更后你都得重新测试。
「现有代码没有测试」你正在改进它。为现有代码添加测试。

示例:修复 Bug

Bug: 空邮箱也被接受

RED

test('rejects empty email', async () => {
  const result = await submitForm({ email: '' });
  expect(result.error).toBe('Email required');
});

验证 RED

$ npm test
FAIL: expected 'Email required', got undefined

GREEN

function submitForm(data: FormData) {
  if (!data.email?.trim()) {
    return { error: 'Email required' };
  }
  // ...
}

验证 GREEN

$ npm test
PASS

重构 如有需要,为多个字段提取验证逻辑。

验证清单

标记工作完成前:

  • 每个新增函数/方法都有测试
  • 实现之前,亲眼看到每个测试失败
  • 每个测试都因预期原因失败(缺少功能,而非拼写错误)
  • 编写了能通过每个测试的最少代码
  • 所有测试均通过
  • 输出干净(没有错误或警告)
  • 测试使用真实代码(仅在无法避免时使用模拟对象)
  • 已覆盖边缘情况和错误
  • 没有挂起的测试进程(pgrep -f "vitest|jest" 返回空结果)

无法勾选所有项?说明你跳过了 TDD。重新开始。

集成与在线验证

当已接受的计划或风险级别超出本地行为范围时,请阅读 references/integration-and-live-proof.md。

如果任务依赖以下内容,单元测试就不够了:

  • 真实 API 调用
  • 已预置或可重置的数据
  • 浏览器或工作进程编排
  • 跨服务副作用
  • 负载或压力行为

在这些情况下,内环仍采用 TDD,并提高外层验证的深度。

覆盖率阈值(项目默认值)

目标:代码覆盖率达到 80% 以上,包括:

  • 分支:80%
  • 函数:80%
  • 行:80%
  • 语句:80%

验证方式: npm run test:coverage 或等效命令。

未达到阈值? 在声称完成之前,补充缺失的测试。

测试优先级

达到 80% 覆盖率需要有意识地确定测试优先级。优先投入精力测试:

  • 面向用户的关键路径(身份验证、支付、数据完整性)
  • 包含多个分支的复杂逻辑
  • 曾经出过问题的代码(容易回归的区域)

不要因为代码“看起来简单”就跳过测试——简单的代码也会出错。80% 是底线,不是上限。

遇到阻碍时

问题解决方案
不知道怎么测试编写期望的 API。先写断言。询问你的人工伙伴。
测试太复杂设计太复杂。简化接口。
必须模拟所有内容代码耦合度太高。使用依赖注入。
测试设置过于庞大提取辅助函数。还是太复杂?简化设计。

为可测试性而设计(测试难以编写时)

如果测试难以编写,就需要改进接口:

  1. 接收依赖,不要自行创建依赖

    • 易于测试:function processOrder(order, paymentGateway) {}
    • 难以测试:function processOrder(order) { const gw = new StripeGateway(); }
  2. 返回结果,不要产生副作用

    • 易于测试:function calculateDiscount(cart): Discount {}
    • 难以测试:function applyDiscount(cart): void { cart.total -= discount; }
  3. 缩小表面积——方法越少,需要的测试就越少;参数越少,设置就越简单

关注行为

测试对象如何协作,而不是它们包含什么。如果测试检查 .state、.length 或私有字段,那它测试的就是结构——当内部实现改变而行为不变时,这类测试就会失效。

测试目标正确错误
函数输出expect(calculate(input)).toBe(result)expect(calculator.internalCache).toContain(...)
组件行为expect(screen.getByText('Saved')).toBeTruthy()expect(component.state.saved).toBe(true)
服务交互expect(response.status).toBe(201)expect(service.callCount).toBe(1)

这已经隐含在“测试实现细节”这一测试异味中。应将其作为默认视角:每个断言都应该回答“用户/调用方观察到了什么?”,而不是“内部发生了什么?”

Agent 之间的测试契约

当 CC10x 将工作分配给多个 Agent(规划者编写测试规范、构建者实现、审查者验证)时,测试文件就是契约:

  • 规划者在计划中通过测试名称和断言定义预期行为
  • 构建者先编写测试,再实现功能直至测试通过——测试文件证明契约已履行
  • 审查者重新运行相同的测试——通过表示契约已履行,失败表示契约已被破坏

不要用文字重复描述契约。如果测试文件已经表达了要求,那么测试文件就是要求本身。

输出格式

## TDD Cycle

### Requirements
[What functionality is being built]

### RED Phase
- Test: [test name]
- Command: `npm test -- --grep "test name"`
- Result: exit 1 (FAIL as expected)
- Failure reason: [function not defined / expected X got Y]

### GREEN Phase
- Implementation: [summary]
- File: [path:line]
- Command: `npm test -- --grep "test name"`
- Result: exit 0 (PASS)

### REFACTOR Phase
- Changes: [what was improved]
- Command: `npm test`
- Result: exit 0 (all tests pass)

最终规则

Production code → test exists and failed first
Otherwise → not TDD

未经你的人工伙伴许可,不得例外。

评论

支持 Markdown