mcpField
D
drill-tdd

drill-tdd

github.com/kucherenko测试与质量8544 次安装1 天前更新
代码仓库

关于 drill-tdd

drill-tdd 是面向功能开发、修复缺陷和重构的测试驱动开发技能,要求先观察测试失败,再编写最少的生产代码,并在测试通过后重构。

主要功能

  1. 01先编写失败测试
  2. 02确认失败原因符合预期
  3. 03编写通过测试所需的最少代码
  4. 04重构前运行测试
  5. 05通过测试复现缺陷

适用场景

  1. 01按测试优先流程开发新功能
  2. 02修复缺陷并防止回归
  3. 03在测试通过的前提下重构

安装

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

$npx skills add https://github.com/kucherenko/gangsta --skill drill-tdd

安装后在 Claude Code 中输入/drill-tdd确认已加载。

详细介绍

训练:测试驱动开发

概述

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

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

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

何时使用

始终适用:

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

例外情况(请示 Don):

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

在想“就这一次跳过训练”?停下。这是在自我合理化。

铁律(测试的沉默法则)

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

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

没有例外:

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

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

红—绿—重构

digraph tdd_cycle {
    rankdir=LR;
    red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
    verify_red [label="Verify fails\ncorrectly", shape=diamond];
    green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
    verify_green [label="Verify passes\nAll green", shape=diamond];
    refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
    next [label="Next", shape=ellipse];

    red -> verify_red;
    verify_red -> green [label="yes"];
    verify_red -> red [label="wrong\nfailure"];
    green -> verify_green;
    verify_green -> refactor [label="yes"];
    verify_green -> green [label="no"];
    refactor -> verify_green [label="stay\ngreen"];
    verify_green -> next;
    next -> red;
}
RED — 编写失败的测试

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

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

名称清晰,测试真实行为,只测试一件事
</Good>

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

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

要求:

  • 每个测试只测试一种行为
  • 名称清晰且具有描述性
  • 使用真实代码(除非无法避免,否则不要使用模拟对象)
验证 RED — 观察它失败

必须执行。绝不可跳过。

运行测试。确认:

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

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

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

GREEN — 编写最少的代码

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

```typescript async function retryOperation(fn: () => Promise): Promise { for (let i = 0; i < 3; i++) { try { return await fn(); } catch (e) { if (i === 2) throw e; } } throw new Error('unreachable'); } ``` 只写足以通过测试的代码 ```typescript async function retryOperation( fn: () => Promise, options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; } ): Promise { // YAGNI — over-engineered } ``` 过度设计

不要添加功能、重构其他代码,也不要超出测试要求去“改进”。

验证 GREEN — 观察它通过

必须执行。

运行测试。确认:

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

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

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

REFACTOR — 清理代码

只有通过测试后才能:

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

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

重复

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

好的测试

质量好差
精简只测试一件事。名称里有“和”?拆开。test('validates email and domain and whitespace')
清晰名称描述行为test('test1')
表达意图展示所需的 API模糊不清,不知道代码应该做什么

为什么顺序很重要

“我会之后再写测试,验证它能正常工作。”

代码写完后再写的测试会立即通过。立即通过证明不了任何事:

  • 可能测试了错误的内容
  • 可能测试的是实现,而不是行为
  • 可能遗漏你没想到的边界情况
  • 你从未看到它捕获 Bug

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

“删除 X 小时的工作太浪费了。”

这是沉没成本谬误。时间已经花掉了。现在你可以选择:

  • 删除并用 TDD 重写(信心更高)
  • 保留代码并在之后添加测试(信心较低,也更可能有 Bug)

真正的“浪费”,是保留你无法信任的代码。

“TDD 是教条主义的,务实就是灵活变通。”

TDD 本身就很务实:

  • 在提交前发现 Bug(比事后调试更快)
  • 防止回归(测试会立即发现问题)
  • 记录行为(测试展示了如何使用代码)
  • 支持重构(可以自由修改,测试会发现问题)

“务实”的捷径 = 在生产环境中调试 = 更慢。

常见借口

借口现实
“太简单,不值得测试”简单的代码也会出错。测试只需 30 秒。
“我之后再测试”测试立即通过说明不了任何问题。
“已经手动测试过了”临时测试 ≠ 系统化测试。没有记录,也无法重新运行。
“删掉 X 小时的成果太浪费了”沉没成本谬误。保留未经验证的代码就是负债。
“留作参考,先写测试”你会在它的基础上改。那就是先实现后测试。删掉就是彻底删除。
“得先探索一下”没问题。丢弃探索成果,从 Drill 开始。
“测试很难写 = 设计不清晰”听听测试的反馈。难以测试 = 难以使用。
“TDD 会拖慢我”Drill 比调试更快。
“现有代码没有测试”你正在改进它。为你改动的部分添加测试。

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

  • 先写代码,再写测试
  • 实现之后才写测试
  • 测试立即通过
  • 无法解释测试失败的原因
  • 测试“稍后”再补
  • 为“就这一次”找借口
  • “留作参考”或“沿用现有代码”
  • “已经花了 X 小时,删掉太浪费了”
  • “TDD 太教条了,我这是讲求实际”

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

示例:修复 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' };
  }
  // ...existing code
}

验证 GREEN

$ npm test
PASS

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

验证清单

标记工作完成前:

  • 每个新增函数/方法都有测试
  • 实现前观察过每个测试失败
  • 每个测试都因预期原因失败(功能缺失,而非拼写错误)
  • 编写了通过每个测试所需的最少代码
  • 所有测试均通过
  • 输出干净无误(没有错误或警告)
  • 测试使用真实代码(只有不得已时才使用模拟对象)
  • 已覆盖边界情况和错误

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

集成调试

发现 Bug?编写失败测试来复现它。遵循 Drill 循环。测试既能证明修复有效,也能防止回归。

绝不要在没有测试的情况下修复 Bug。先使用 gangsta:interrogation-debugging 调查根本原因。

遇到困难时

问题解决方案
不知道怎么测试写出你希望拥有的 API。先写断言。向 Don 请教。
测试太复杂设计太复杂。简化接口。
什么都必须模拟代码耦合过紧。使用依赖注入。
测试设置太庞大提取辅助方法。仍然复杂?简化设计。

Omerta 合规

  • 真相原则:测试输出是实际命令输出,而非口头声称
  • 规范即法律:每项实现都能追溯到测试,而测试能追溯到需求

评论

支持 Markdown