测试驱动开发Skill test-driven-development

测试驱动开发是一种软件开发方法,强调先编写测试代码以验证预期行为,然后编写足够的生产代码以通过测试,最后进行重构以优化代码结构。这种方法有助于提高代码质量和可维护性,确保代码变更的正确性。

测试 0 次安装 0 次浏览 更新于 3/2/2026

name: test-driven-development description: 测试驱动开发

测试驱动开发

先写测试。看它失败。写最少的代码通过。重构。

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

铁律

没有失败的测试,就不要有改变行为的生产代码

写了测试之前的代码?完全删除它。从测试重新开始实现。

**重构是例外:**重构步骤改变结构,不改变行为。测试保持绿色。不需要新的失败测试。

红-绿-重构循环

RED ──► 验证失败 ──► GREEN ──► 验证通过 ──► REFACTOR ──► 验证通过 ──► 下一个 RED
         │                         │                            │
         ▼                         ▼                            ▼
      错误的失败?          仍然失败?              破坏了测试?
      修复测试,重试         修复代码,重试            修复,重试

RED - 编写失败的测试

为一个行为编写一个最小的测试。

好例子:

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

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

**要求:**一个行为。清晰的名称。真实代码(如果不可避免,只使用模拟)。

验证RED - 看它失败

强制的。绝不能跳过。

npm test path/to/test.test.ts

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

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

不可接受的:运行时设置错误,导入失败,环境问题。

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

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

过度工程化超出测试要求

只写测试要求的。没有额外功能,没有“改进”。

验证GREEN - 看它通过

强制的。

npm test path/to/test.test.ts

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

测试失败?修复代码,不是测试。 其他测试失败?现在修复,然后再继续。

REFACTOR - 清理

只有在绿色之后:去除重复。改善名称。提取助手。

保持测试绿色。不添加新行为。

重复

下一个失败的测试为下一个行为。

好的测试

**最小:**每个测试一件事。名称中的“和”?分开它。❌ test('validates email and domain and whitespace')

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

**显示意图:**展示期望的API使用,而不是实现细节。

示例: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

REFACTOR: 如果模式重复,提取验证助手。

红旗 - 停止并重新开始

这些中的任何一个意味着删除代码并用TDD重新开始:

  • 测试前写的代码
  • 测试立即通过(测试现有行为)
  • 不能解释为什么测试失败
  • 合理化“就这一次”或“这次不同”
  • 保留代码“作为参考”同时写测试
  • 声称“测试后达到同样的目的”

卡住时

问题 解决方案
不知道如何测试 写下你希望存在的API。先写断言。
测试太复杂 设计太复杂。简化接口。
必须模拟一切 代码太耦合。引入依赖注入。
测试设置巨大 提取助手。仍然复杂?简化设计。

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

铁律(“删除并重新开始”)适用于你写的没有测试的新代码。对于没有测试的继承代码,使用特性化测试:

  1. 编写测试以捕获当前行为(即使“错误”)
  2. 运行测试,观察实际输出
  3. 更新断言以匹配现实(这些是“金主”)
  4. 现在你有了一个安全网,可以重构
  5. 为新的行为变化应用TDD

特性化测试锁定现有行为,以便你可以安全地重构。它们是入口,不是永久状态。

易变规则

测试必须是确定性的。禁止在单元测试中使用这些:

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

易变测试?修复或删除。易变测试侵蚀对整个套件的信任。

集成调试

发现bug?先写一个失败的测试来重现它。然后遵循TDD循环。测试证明修复并防止回归。

计划:测试列表

在进入循环之前,花2分钟列出你期望写的下一个3-10个测试。这可以防止局部最优设计,其中早期测试将你逼入角落。

示例测试列表,用于重试功能:

  • 在失败时重试N次
  • 在成功时返回结果
  • 在最大重试次数耗尽后抛出
  • 在尝试之间调用onRetry回调
  • 尊重退避延迟

按顺序完成列表。根据学习添加/删除测试。

测试反模式

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

哲学和合理化

对于常见反对意见的详细反驳(“我将在之后测试”,“删除工作是浪费”,“TDD是教条的”):见references/tdd-philosophy.md

最终规则

如果生产代码存在 → 测试首先存在并且首先失败
否则 → 不是TDD