Files
VibeEngineering/.claude/skills/test-driven-development/SKILL.md
闫旭隆 c484cafb45 Initial commit: VibeEngineering V2
- 两阶段分离:设计阶段人工确认,执行阶段全自动化
- 子代理驱动:Implementer → Spec Reviewer → Quality Reviewer
- 原生 Task 系统:使用 Claude Code Task 替代自定义状态管理
- 跨 Compact 恢复:PreCompact + SessionStart Hook(内联命令实现)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 18:00:55 +08:00

6.1 KiB
Raw Blame History

name, description
name description
test-driven-development 在实现任何功能或修复 bug 时使用,在写实现代码之前

测试驱动开发 (TDD)

概述

先写测试。看它失败。写最小代码通过测试。

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

违反规则的字面意思就是违反规则的精神。

何时使用

总是:

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

例外(需要用户确认):

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

想着"就这一次跳过 TDD"?停下来。那是在找借口。

铁律

没有失败的测试,就不能写生产代码

先写代码再写测试?删除它。重新开始。

没有例外:

  • 不要把它作为"参考"保留
  • 不要在写测试时"调整"它
  • 不要看它
  • 删除就是删除

从测试重新实现。就这样。

RED-GREEN-REFACTOR

RED写失败测试
    ↓ 验证失败正确
GREEN最小代码
    ↓ 验证通过全绿
REFACTOR清理
    ↓ 保持绿色
→ 下一个 → RED

RED - 写失败的测试

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

好的示例:

test('重试失败操作 3 次', 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())
    .mockResolvedValueOnce('success');
  await retryOperation(mock);
  expect(mock).toHaveBeenCalledTimes(2);
});

模糊的名称,测试 mock 而不是代码

要求:

  • 一个行为
  • 清晰的名称
  • 真实代码(除非不可避免,否则不用 mock

验证 RED - 看它失败

强制执行。永远不要跳过。

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 - 过度设计
}

不要添加功能、重构其他代码,或"改进"超出测试范围的内容。

验证 GREEN - 看它通过

强制执行。

npm test path/to/test.test.ts

确认:

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

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

其他测试失败? 现在修复。

REFACTOR - 清理

只有在绿色之后:

  • 移除重复
  • 改进名称
  • 提取辅助函数

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

重复

下一个失败测试,测试下一个功能。

好的测试

质量
最小 一件事。名称中有"和"?拆分它。 test('验证邮箱和域名和空格')
清晰 名称描述行为 test('test1')
展示意图 展示期望的 API 隐藏代码应该做什么

为什么顺序很重要

"我之后写测试来验证它能工作"

事后写的测试立即通过。立即通过什么都证明不了:

  • 可能测试了错误的东西
  • 可能测试的是实现,不是行为
  • 可能遗漏了你忘记的边界情况
  • 你从未看到它捕获 bug

先测试强迫你看到测试失败,证明它确实在测试什么。

常见借口

借口 现实
"太简单不需要测试" 简单代码也会出错。测试只需 30 秒。
"我之后再测试" 测试立即通过什么都证明不了。
"已经手动测试过了" 临时 ≠ 系统化。没有记录,无法重新运行。
"删除 X 小时的工作太浪费" 沉没成本谬误。保留不可信的代码才是浪费。
"TDD 太教条了,务实意味着灵活" TDD 就是务实的:更快发现 bug防止回归。
"事后测试能达到相同目的" 不。事后测试回答"这做了什么?"先测试回答"这应该做什么?"

危险信号 - 停下来重新开始

  • 先写代码再写测试
  • 实现后才写测试
  • 测试立即通过
  • 无法解释测试为什么失败
  • 测试是"之后"添加的
  • 合理化"就这一次"
  • "我已经手动测试过了"
  • "保留作为参考"或"调整现有代码"

所有这些意味着:删除代码。用 TDD 重新开始。

验证清单

完成工作前:

  • 每个新函数/方法都有测试
  • 在实现之前看到每个测试失败
  • 每个测试失败的原因是预期的(功能缺失,不是拼写错误)
  • 为每个测试写了最小代码通过
  • 所有测试通过
  • 输出干净(没有错误、警告)
  • 测试使用真实代码mock 只在不可避免时使用)
  • 边界情况和错误都有覆盖

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

卡住时

问题 解决方案
不知道如何测试 先写期望的 API。先写断言。问用户。
测试太复杂 设计太复杂。简化接口。
必须 mock 所有东西 代码耦合太紧。使用依赖注入。
测试设置太大 提取辅助函数。仍然复杂?简化设计。

最终规则

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

没有用户许可不能有例外。