mcpField
T
test-first

test-first

github.com/noobygains测试与质量11561 次安装5 天前更新
代码仓库

关于 test-first

test-first 是引导开发者通过失败测试、最小实现和重构来完成代码变更的技能。它适用于功能开发、修复缺陷、重构和行为修改,少数例外需经人工同意。

主要功能

  1. 01先写失败测试,再写生产代码
  2. 02验证预期失败与通过结果
  3. 03仅实现通过测试所需的最少代码
  4. 04在测试持续通过时进行重构
  5. 05用测试复现并防止缺陷

适用场景

  1. 01按测试优先步骤开发新功能
  2. 02用复现测试修复缺陷
  3. 03通过测试检查重构回归
  4. 04通过已验证测试修改行为

安装

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

$npx skills add https://github.com/noobygains/godmode --skill test-first

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

附带文件testing-anti-patterns.md

详细介绍

测试优先开发

概述

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

核心原则: 如果你没有亲眼看到测试失败,就没有证据证明它验证了正确的行为。

没有例外。没有变通办法。没有捷径。

适用场景

必须采用:

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

例外情况(请与人工协作者确认):

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

是不是很想说“就这一次,跳过测试优先”?停下。这是在自我开脱。

首要指令

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

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

零容忍:

  • 不要把它留作“参考资料”
  • 不要在编写测试时对它做“调整”
  • 不要看它
  • 删除就是删除

从测试开始重新编写。到此为止。

红—绿—重构

digraph tdd_loop {
    rankdir=LR;
    red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
    check_red [label="Fails\ncorrectly?", shape=diamond];
    green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
    check_green [label="All tests\npass?", shape=diamond];
    refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
    next [label="Next", shape=ellipse];

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

编写一个描述预期行为的最小测试。

```typescript test('retries failed operations up to 3 times', async () => { let callCount = 0; const unstableOp = () => { callCount++; if (callCount < 3) throw new Error('transient failure'); return 'done'; };

const outcome = await withRetry(unstableOp);

expect(outcome).toBe('done'); expect(callCount).toBe(3); });

名称清晰,测试实际行为,只关注一个方面
</Good>

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

名称含糊,验证的是模拟对象的交互,而非实际行为

标准:

  • 每个测试只验证一种行为
  • 名称清晰
  • 使用真实代码(仅在无法避免时使用模拟对象)
验证 RED - 观察测试失败

必须执行。绝不可跳过。

npm test path/to/test.test.ts

确认:

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

测试通过了? 你测试的是现有行为。重写测试。

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

GREEN - 编写最少代码

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

```typescript async function withRetry(fn: () => Promise): Promise { for (let attempt = 0; attempt < 3; attempt++) { try { return await fn(); } catch (e) { if (attempt === 2) throw e; } } throw new Error('unreachable'); } ``` 只写足以通过测试的代码 ```typescript async function withRetry( fn: () => Promise, opts?: { maxAttempts?: number; backoffStrategy?: 'linear' | 'exponential'; onAttempt?: (n: number) => void; } ): Promise { // YAGNI } ``` 过度设计

不要添加功能、重构无关代码,也不要超出测试要求进行“改进”。

验证 GREEN - 观察测试通过

必须执行。

npm test path/to/test.test.ts

确认:

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

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

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

REFACTOR - 清理代码

仅在测试通过后:

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

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

重复

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

质量标准

质量好坏
专注一次只做一件事。名称里有“和”?拆开它。test('validates email and domain and whitespace')
描述清晰名称说明所验证的行为test('test1')
有明确目的展示期望的 API掩盖代码应该做什么

为什么顺序很重要

“我稍后再写测试,确认它能正常运行”

代码写完后再编写的测试会立即通过。立即通过什么也证明不了:

  • 可能验证了错误的内容
  • 可能验证的是实现细节,而不是行为
  • 可能遗漏了你忽视的边界情况
  • 你从未观察过测试捕获失败的过程

先写测试会迫使你观察失败,从而证明测试确实验证了某些内容。

“我已经手动测试了所有边界情况”

手动测试是临时随意的。你以为自己面面俱到,但实际上:

  • 没有测试内容的记录
  • 代码变更后无法重新运行
  • 有压力时很容易漏掉某些情况
  • “我试过了,能用”并不全面

自动化测试是系统化的。每次执行的结果都相同。

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

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

  • 删除并采用测试先行的方式重做(再花 X 小时,但信心十足)
  • 保留代码再补上测试(30 分钟,信心不足,还可能存在缺陷)

真正的浪费,是保留自己无法信任的代码。没有正当测试的可运行代码就是技术债务。

“测试先行太教条了,务实就该灵活调整”

测试先行确实很务实:

  • 在提交前发现缺陷(比之后调试更快)
  • 防止回归(测试会立即捕捉到破坏)
  • 记录行为(测试展示代码如何运行)
  • 支持重构(可以随意修改,测试会捕捉问题)

“务实”的捷径 = 在线上调试 = 更慢。

“事后编写测试也能达到同样的目的——重在精神,而非仪式”

不对。事后测试回答“这段代码做什么?”,先写测试回答“这段代码应该做什么?”

事后测试会受到你的实现影响。你验证的是自己构建的内容,而不是实际需求。你检查的是自己记得的边界情况,而不是后来才发现的情况。

先写测试会迫使你在实现之前发现边界情况。事后测试只能验证你是否记得所有情况(你没有)。

花 30 分钟事后写测试不等于测试先行。你能获得覆盖率,但无法证明测试有效。

认知陷阱

自我辩解实际情况
“太简单了,不值得测试”简单代码也会出错。编写测试只需 30 秒。
“我稍后再测试”测试立即通过什么也证明不了。
“事后测试也能达到同样的目的”事后测试 =“这段代码做什么?”先写测试 =“这段代码应该做什么?”
“我已经手动测试过了”临时测试并不系统。没有记录,也无法重新运行。
“删掉 X 小时的工作太浪费了”沉没成本谬误。保留未经验证的代码就是技术债务。
“保留代码作参考,先写测试”你会根据它调整实现。这就是事后测试。删除就意味着删除。
“得先探索一下”可以。丢弃探索的成果,然后从测试先行开始。
“难以测试 = 我应该跳过测试”听从测试的反馈。难以测试 = 难以使用。简化设计。
“测试先行会拖慢我”测试先行比调试更快。务实 = 测试先行。
“手动测试更快”手动测试无法证明边界情况正确。每次修改后都得重新测试。
“现有代码没有测试”你正在改进它。为现有代码添加测试。

防护措施——停止并重新开始

  • 先写代码,再写测试
  • 实现完成后再写测试
  • 测试立即通过
  • 无法解释测试为什么失败
  • “稍后”再添加测试
  • 为“就这一次”找理由
  • “我已经手动测试过了”
  • “事后测试的作用一样”
  • “重在精神,而非仪式”
  • “保留代码作参考”或“调整现有代码”
  • “已经投入 X 小时,删除太浪费了”
  • “测试先行太教条了,我是在务实”
  • “这次情况不同,因为……”

这些情况都意味着:删除代码,从测试先行重新开始。

示例:修复 Bug

Bug: 表单接受空电子邮件地址

RED

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

验证 RED

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

GREEN

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

验证 GREEN

$ npm test
PASS

重构 如果需要,提取一个用于多个字段的验证流程。

完成检查清单

宣布工作完成前:

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

无法勾选每一项?你跳过了测试优先。重新开始。

遇到困难时

问题解决方案
不知道如何编写测试编写你希望存在的 API。先写断言。向你的人类搭档求助。
测试太复杂设计太复杂。简化接口。
必须模拟所有内容代码耦合过紧。使用依赖注入。
测试设置过于庞大提取辅助函数。还是太繁琐?简化设计。

集成调试

发现 Bug?编写一个能复现它的失败测试。遵循测试优先循环。该测试既能验证修复,也能防止回归。

绝不在没有测试的情况下修复 Bug。

测试反模式

添加模拟或测试工具时,请阅读 @testing-anti-patterns.md,避免常见陷阱:

  • 验证模拟行为,而非真实行为
  • 在生产类中添加仅供测试使用的方法
  • 未了解依赖链就进行模拟

最终规则

Production code -> test exists and failed first
Otherwise -> not test-first

未经你的人类搭档许可,不得破例。

评论

支持 Markdown