mcpField
T
test-driven-development

test-driven-development

github.com/izyanrajwani测试与质量481 次安装2 周前更新
代码仓库

关于 test-driven-development

面向实现或修复软件行为的开发者的测试驱动开发 Skill。它要求先观察测试失败,再编写会改变行为的生产代码,并遵循红—绿—重构循环。

主要功能

  1. 01行为变更前必须先有失败测试
  2. 02规定红—绿—重构循环
  3. 03说明有效测试失败的判定标准
  4. 04涵盖遗留代码特征测试
  5. 05提供确定性测试规则

适用场景

  1. 01先编写测试再实现功能
  2. 02先复现缺陷再进行修复
  3. 03用特征测试安全重构遗留代码

安装

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

$npx skills add https://github.com/izyanrajwani/agent-skills-library --skill test-driven-development

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

附带文件references/tdd-philosophy.mdreferences/testing-anti-patterns.md

详细介绍

测试驱动开发

先写测试。观察测试失败。编写能通过测试的最少代码。然后重构。

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

铁律

NO BEHAVIOR-CHANGING PRODUCTION CODE WITHOUT A FAILING TEST FIRST

先写了代码再写测试?彻底删除代码。从测试重新开始实现。

重构除外: 重构步骤改变的是结构,而不是行为。测试始终保持通过。无需新增失败测试。

红—绿—重构循环

RED ──► Verify Fail ──► GREEN ──► Verify Pass ──► REFACTOR ──► Verify Pass ──► Next RED
         │                         │                            │
         ▼                         ▼                            ▼
      Wrong failure?           Still failing?              Broke tests?
      Fix test, retry          Fix code, retry             Fix, retry
红 — 编写失败的测试

针对一种行为编写一个最简测试。

好示例:

test('retries failed operations 3 times', async () => {
  let attempts = 0;
  const operation = async () => {
    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);
});

名称含糊,只断言调用次数而不验证结果,测试的是模拟机制而非行为

要求: 一种行为。名称清晰。使用真实代码(只有在无法避免时才使用模拟)。

验证红 — 观察测试失败

必须执行。绝不跳过。

npm test path/to/test.test.ts

测试必须因正确的原因变红。可接受的红色状态:

  • 断言失败(预期行为尚未实现)
  • 编译/类型错误(函数尚不存在)

不可接受:运行时初始化错误、导入失败、环境问题。

测试立即通过?你测的是已有行为——修正测试。 测试因错误的原因报错?修复错误,重新运行,直到它正确失败。

绿 — 编写最少代码

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

好示例:

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'; }
): Promise<T> { /* YAGNI */ }

实现过度,超出测试要求

只编写测试所要求的内容。不添加额外功能,不做“改进”。

验证绿 — 观察测试通过

必须执行。

npm test path/to/test.test.ts

确认:测试通过。其他所有测试仍然通过。输出干净(没有错误或警告)。

测试失败?修复代码,不要改测试。 其他测试失败?继续之前立即修复。

重构 — 清理代码

只有测试通过后才能重构:消除重复。改进命名。提取辅助函数。

始终保持测试通过。不添加新行为。

重复

针对下一种行为编写下一个失败的测试。

好测试

最小化: 每个测试只测一件事。名称里有“和”?拆开。❌ test('validates email and domain and whitespace')

清晰: 名称描述行为。❌ test('test1')

体现意图: 展示期望的 API 用法,而不是实现细节。

示例:修复缺陷

缺陷: 接受空邮箱

红:

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

验证红:

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

绿:

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

验证绿:

$ npm test
PASS

重构: 如果这种模式重复出现,就提取验证辅助函数。

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

出现以下任一情况,都意味着应删除代码并从 TDD 重新开始:

  • 先写了代码,再写测试
  • 测试立即通过(测试的是已有行为)
  • 无法解释测试为何失败
  • 为“就这一次”或“情况不同”找理由
  • 写测试时把代码留着“当参考”
  • 声称“事后补测试也能达到同样目的”

遇到困难时

问题解决方案
不知道如何测试写出你希望存在的 API。先写断言。
测试太复杂设计太复杂。简化接口。
什么都必须模拟代码耦合过紧。引入依赖注入。
测试初始化内容太多提取辅助函数。还是复杂?简化设计。

遗留代码(没有现有测试)

铁律(“删除并重新开始”)适用于你在没有测试的情况下编写的新代码。对于没有测试的继承代码,请使用特征测试:

  1. 编写测试以记录当前行为(即使它“有误”)
  2. 运行测试,观察实际输出
  3. 更新断言以匹配实际情况(这些就是“黄金主测试”)
  4. 现在你有了一道安全网,可以放心重构
  5. 对新的行为变更应用 TDD

特征测试会锁定现有行为,让你能够安全地重构。它们是入门途径,而非永久状态。

不稳定性规则

测试必须具有确定性。单元测试中禁止以下做法:

  • 真实睡眠/延迟 → 使用模拟计时器(vi.useFakeTimers()、jest.useFakeTimers())
  • 墙上时钟时间 → 注入时钟,并针对注入的时间进行断言
  • Math.random() → 为 RNG 设定种子或注入 RNG
  • 网络调用 → 在边界处模拟,或使用 MSW
  • 文件系统竞态条件 → 使用名称唯一的临时目录

测试不稳定?修复它或删除它。不稳定的测试会侵蚀整个测试套件的可信度。

调试集成

发现 Bug?先编写一个能复现它的失败测试。然后遵循 TDD 循环。测试既能证明修复有效,也能防止回归。

规划:测试列表

开始循环之前,花 2 分钟列出接下来预计要编写的 3-10 个测试。这可以避免局部最优设计,防止早期测试让你陷入困境。

重试函数的测试列表示例:

  • 失败时重试 N 次
  • 成功时返回结果
  • 达到最大重试次数后抛出异常
  • 在多次尝试之间调用 onRetry 回调
  • 遵循退避延迟

按顺序完成列表中的测试。随着了解的深入,添加或移除测试。

测试反模式

编写涉及模拟对象、依赖项或测试工具的测试时:请参阅 references/testing-anti-patterns.md,了解常见陷阱,包括测试模拟行为,以及向生产类添加仅供测试使用的方法。

理念与合理化

有关对常见异议(“我之后再测试”、“删除成果很浪费”、“TDD 教条”)的详细反驳:请参阅 references/tdd-philosophy.md

最终规则

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

评论

支持 Markdown