tcrdd
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTDD — Test Driven Development
TDD — 测试驱动开发
The Three Rules (Uncle Bob)
三大规则(Uncle Bob)
Sources:
- http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd
- https://blog.cleancoder.com/uncle-bob/2014/12/17/TheCyclesOfTDD.html
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
来源:
- http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd
- https://blog.cleancoder.com/uncle-bob/2014/12/17/TheCyclesOfTDD.html
- 除非是为了让失败的单元测试通过,否则不允许编写任何生产代码。
- 不允许编写超出足以导致失败的单元测试内容;编译失败也属于失败。
- 不允许编写超出足以让当前单个失败单元测试通过的生产代码。
The Four Nested Cycles
四个嵌套循环
The Three Rules operate at the finest granularity, but TDD is structured as four nested cycles at different timescales. Understanding all four prevents the most common failure modes.
| Cycle | Timescale | Name | What you do |
|---|---|---|---|
| Nano | Seconds | Three Rules | Write one failing line → pass it → repeat |
| Micro | Minutes | Red / Green / Refactor | Complete one unit test → make it pass → clean up |
| Milli | ~10 minutes | Specific / Generic | Check that production code is growing more general, not mirroring the tests |
| Primary | ~1 hour | Architectural Boundaries | Step back and verify you haven't crossed or blurred a major architectural boundary |
三大规则是最细粒度的操作准则,但TDD实际上由四个不同时间尺度的嵌套循环构成。理解这四个循环可以避免绝大多数常见的使用误区。
| 周期 | 时间尺度 | 名称 | 操作内容 |
|---|---|---|---|
| 纳秒级 | 秒级 | 三大规则 | 编写一行失败的测试代码 → 让它通过 → 重复 |
| 微级 | 分钟级 | 红 / 绿 / 重构 | 完成一个单元测试 → 让它通过 → 清理代码 |
| 毫级 | 约10分钟 | 特定 / 通用 | 检查生产代码是否越来越通用,而不是为了适配测试而编写 |
| 主周期 | 约1小时 | 架构边界 | 退一步验证你没有跨越或模糊主要的架构边界 |
Nano-cycle (seconds) — The Three Rules
纳秒级循环(秒级)—— 三大规则
The rules create a tight loop measured in seconds. At no point does the system stop compiling or all tests stop passing for more than a minute. If you walked up to any developer at any random moment, their code worked a minute ago.
这些规则构成了以秒为单位的紧密循环。任何时候系统不会出现超过1分钟的编译失败或所有测试不通过的情况。如果你随机找到任何一位开发人员,他们的代码一分钟前肯定是可以正常运行的。
Micro-cycle (minutes) — Red / Green / Refactor
微级循环(分钟级)—— 红/绿/重构
Once a full unit test is in place, apply RGR:
- Red — Have a failing test.
- Green — Write the minimum code to make it pass. Don't worry about structure yet.
- Refactor — Clean up the mess. The tests are your safety net.
Refactoring is not a phase at the end of the project — it happens every few minutes, continuously.
当完整的单元测试编写完成后,应用RGR流程:
- 红 — 编写一个失败的测试。
- 绿 — 编写最少的代码让测试通过,暂时不用考虑代码结构。
- 重构 — 清理代码冗余,测试就是你的安全保障。
重构不是项目末期才做的工作——它每隔几分钟就会持续进行一次。
Milli-cycle (~10 min) — Specific / Generic
毫级循环(约10分钟)—— 特定/通用
As the tests get more specific, the code gets more generic.
Watch for the symptom of over-specificity: production code that starts to resemble the test data. A healthy sign is that tests you haven't written yet would already pass. An unhealthy sign is that each new test forces a narrowly targeted or special case.
ifIf you get stuck — meaning the next test would require a large out-of-cycle rewrite — backtrack. Delete recent tests, find an earlier branching point, and approach it with smaller, more general increments.
测试越具体,代码应该越通用。
要注意过度特定化的信号:生产代码开始变得和测试数据高度相似。健康的信号是你还没编写的测试也已经能通过,不健康的信号是每一个新测试都需要新增针对性的判断或特殊处理逻辑。
if如果你卡住了——意味着下一个测试需要大量超出常规周期的重写——那就回退。删除最近的测试,回到更早的分支点,用更小、更通用的增量来推进。
Primary cycle (~1 hour) — Architectural Boundaries
主周期(约1小时)—— 架构边界
Every hour or so, zoom out. Ask: are we drifting across a boundary we should be protecting? The nano and micro cycles are too fine-grained to catch architectural drift. Use this cycle to check alignment with the intended clean architecture, and let those decisions guide the next hour's nano/micro/milli work.
每小时左右退出来宏观审视一下,问问自己:我们是不是正在越过本该遵守的边界?纳秒级和微级循环的粒度太细,无法察觉到架构偏移。用这个周期来检查是否符合预期的整洁架构,用这些决策来指导下一个小时的纳秒/微/毫级循环工作。
How to Apply the Rules When Writing Code
编写代码时如何应用这些规则
Step 1 — Write a failing test (Rule 1 + Rule 2)
步骤1 — 编写一个失败的测试(规则1 + 规则2)
- Pick the smallest next behaviour you want to add.
- Write only enough of a test to fail. As soon as it won't compile, or an assertion fails — stop.
- Do not write the full test scenario at once; get it to a red state first.
- 选择你要添加的最小的下一个功能点。
- 只编写刚好能让测试失败的代码,一旦它无法编译或者断言失败——就停下来。
- 不要一次性写完完整的测试场景,先让它进入失败状态。
Step 2 — Write the minimum production code (Rule 3)
步骤2 — 编写最少的生产代码(规则3)
- Write only the code that makes the currently failing test pass. Nothing more.
- Resist the urge to generalise, add helpers, or handle future cases — those are for later tests.
- If the simplest passing implementation feels like a "cheat" (e.g., returning a hardcoded value), that's fine. The next test will force you to generalise.
- 只编写能让当前失败的测试通过的代码,不要多写任何内容。
- 不要忍不住去做通用化、添加辅助函数或者处理未来的场景——这些都是后续测试要做的事。
- 如果最简单的通过实现看起来像“作弊”(比如返回硬编码值),完全没问题,下一个测试会迫使你做通用化处理。
Step 3 — Refactor
步骤3 — 重构
- With all tests green, clean up both production and test code freely.
- The tests act as a safety net; you can refactor without fear of breaking behaviour.
- 所有测试都通过后,自由清理生产代码和测试代码。
- 测试就是安全网,你可以放心重构而不用担心破坏现有功能。
Step 4 — Repeat
步骤4 — 重复
- Go back to Step 1. Pick the next smallest behaviour.
- Keep cycles short. If you feel stuck for more than a few minutes, the test increment is too big — break it down further.
- 回到步骤1,选择下一个最小的功能点。
- 保持短循环。如果你卡住超过几分钟,说明测试增量太大——把它拆分成更小的部分。
Key Principles
核心原则
Tests are the design. Following TDD forces decoupling. Code that is testable in isolation is, by definition, decoupled. If a module is hard to test, that is a design signal — not a testing problem.
Tests are living documentation. Each test is a precise, executable example of how the system works. They are more useful than prose documentation because they cannot go out of sync with the production code.
"Untestable code" is not a valid excuse. If something seems hard to test, the answer is: (a) find a way to test it as-is, or (b) change the design so it becomes testable. Accepting untestable code is a slippery slope toward abandoning the discipline entirely.
Bug fixing follows the same rules. Before fixing a bug, write a failing test that reproduces it. The fix is only the code needed to make that test pass.
测试就是设计。 遵循TDD会强制代码解耦,可单独测试的代码从定义上来说就是解耦的。如果一个模块很难测试,那是设计信号——不是测试的问题。
测试是活的文档。 每个测试都是系统运行方式的精确可执行示例,它们比文字文档更有用,因为它们永远不会和生产代码不同步。
“无法测试的代码”不是合理的借口。 如果某样东西看起来很难测试,解决方案是:(a) 找到现有状态下的测试方法,或者 (b) 调整设计让它变得可测试。接受无法测试的代码是完全放弃这套规范的滑坡起点。
Bug修复遵循同样的规则。 修复bug之前,先编写一个能复现bug的失败测试,修复工作就是编写刚好能让这个测试通过的代码。
Reviewing Code for TDD Compliance
评审代码是否符合TDD规范
When reviewing code or a PR with TDD in mind, check:
- Is there a test for every piece of production behaviour?
- Does each test assert one specific behaviour (not a giant integration scenario)?
- Could any production code be deleted without a test failing?
- Is the production code over-engineered relative to what the tests require?
- Are there any untested paths that suggest production code was written without a test first?
- Is the code decoupled enough to be tested in isolation (no hidden global state, hard-coded dependencies, etc.)?
从TDD角度评审代码或PR时,检查以下几点:
- 每一段生产功能都有对应的测试吗?
- 每个测试都只断言一个特定的行为(不是巨型集成场景)吗?
- 有没有可以删除但不会导致测试失败的生产代码?
- 生产代码相对于测试要求来说是不是过度设计了?
- 有没有未测试的路径,说明生产代码不是测试先行编写的?
- 代码是不是足够解耦,可以单独测试(没有隐藏的全局状态、硬编码依赖等)?
Common Pitfalls
常见误区
| Pitfall | What the rules say |
|---|---|
| Writing the whole test before running it | Stop as soon as the test fails to compile or an assertion fails (Rule 2) |
| Writing more production code "while I'm here" | Only write what makes the current failing test pass (Rule 3) |
| Skipping tests for "obvious" code | No production code without a failing test first (Rule 1) |
| Writing tests after the fact | Tests written after give you false confidence; the design wasn't shaped by them |
| Large test increments | If an increment takes >10 minutes, break it into smaller steps |
| Getting stuck on the next test | Production code is too specific — backtrack, delete recent tests, generalise |
| Production code mirrors test data | Tests are getting specific faster than code is getting general (milli-cycle violation) |
| 误区 | 规则说明 |
|---|---|
| 运行测试前写完整个测试用例 | 测试编译失败或断言失败时就立刻停下(规则2) |
| “顺便”多写一些生产代码 | 只编写能让当前失败测试通过的代码(规则3) |
| 给“显而易见”的代码跳过测试 | 没有失败的测试就不能写生产代码(规则1) |
| 事后补写测试 | 事后补写的测试会给你错误的信心,代码设计并没有被测试塑造 |
| 测试增量太大 | 如果一个增量耗时超过10分钟,把它拆分成更小的步骤 |
| 下一个测试卡住了 | 生产代码太特定——回退、删除最近的测试、做通用化处理 |
| 生产代码和测试数据高度相似 | 测试变得特定的速度超过了代码变得通用的速度(违反毫级循环规则) |
Example Skeleton (TypeScript)
示例框架(TypeScript)
typescript
// 1. Write a failing test — stops here: Stack doesn't exist yet
describe("Stack", () => {
it("is empty on creation", () => {
const stack = new Stack<number>();
expect(stack.isEmpty()).toBe(true); // RED: Stack is undefined
});
});
// 2. Write minimum production code to pass
class Stack<T> {
isEmpty(): boolean {
return true; // trivially passes — that's fine for now
}
}
// 3. Refactor if needed, then write the next failing test
it("is not empty after push", () => {
const stack = new Stack<number>();
stack.push(1);
expect(stack.isEmpty()).toBe(false); // RED: push doesn't exist, forces real impl
});
// 4. Minimum production code to pass the new test
class Stack<T> {
private items: T[] = [];
isEmpty(): boolean {
return this.items.length === 0;
}
push(item: T): void {
this.items.push(item);
}
}Each test drives the next tiny increment of real behaviour.
typescript
// 1. Write a failing test — stops here: Stack doesn't exist yet
describe("Stack", () => {
it("is empty on creation", () => {
const stack = new Stack<number>();
expect(stack.isEmpty()).toBe(true); // RED: Stack is undefined
});
});
// 2. Write minimum production code to pass
class Stack<T> {
isEmpty(): boolean {
return true; // trivially passes — that's fine for now
}
}
// 3. Refactor if needed, then write the next failing test
it("is not empty after push", () => {
const stack = new Stack<number>();
stack.push(1);
expect(stack.isEmpty()).toBe(false); // RED: push doesn't exist, forces real impl
});
// 4. Minimum production code to pass the new test
class Stack<T> {
private items: T[] = [];
isEmpty(): boolean {
return this.items.length === 0;
}
push(item: T): void {
this.items.push(item);
}
}每个测试驱动下一个微小的真实功能增量。
TCRDD — TCR + TDD
TCRDD — TCR + TDD
TCRDD = TCR (test && commit || revert) + TDD (Test Driven Development).
It blends the two disciplines so that:
- You always develop the right thing (TDD's guarantee)
- You're encouraged to take baby steps, because reverting wipes out wrong work fast (TCR's guarantee)
The tool that automates this workflow is .
git-gambleTCRDD = TCR (test && commit || revert) + TDD (测试驱动开发)。
它融合了两种方法论的优势:
- 你永远在开发“正确”的功能(TDD的保障)
- 鼓励你采取小步迭代,因为回滚会快速清除错误的工作(TCR的保障)
自动化这套工作流的工具是。
git-gambleThe Three Phases
三个阶段
TCRDD cycles through three phases. Each phase ends in either a commit (success) or a revert (failure → retry).
TCRDD在三个阶段之间循环,每个阶段最终要么提交(成功)要么回滚(失败→重试)。
🔴 Red Phase — Write one failing test
🔴 红阶段 — 编写一个失败的测试
Goal: produce exactly one new failing test.
- Write a single test
- Run — this gambles that tests will fail
git gamble --red - Actually run tests
- Tests pass → revert (your test wasn't really new/failing), write another test, repeat
- Tests fail → commit, move to Green
The revert-on-pass is the key TCR twist: if your "new" test passes immediately, you probably didn't add real coverage. Revert and try again.
目标:生成恰好一个新的失败测试。
- 编写单个测试
- 运行— 这相当于“赌”测试会失败
git gamble --red - 实际运行测试
- 测试通过 → 回滚(你的测试并不是真的新的/失败的),编写另一个测试,重复
- 测试失败 → 提交,进入绿阶段
测试通过就回滚是TCR的关键设定:如果你的“新”测试立刻就通过了,说明你可能并没有添加真实的测试覆盖,回滚后重新尝试。
🟢 Green Phase — Make all tests pass
🟢 绿阶段 — 让所有测试通过
Goal: write the minimum code to make the failing test pass.
- Write the minimum code
- Run — gambles that tests will pass
git gamble --green - Actually run tests
- Tests fail → revert, try something else, repeat
- Tests pass → commit, move to Refactor
Write only enough code to go green. No gold-plating.
目标:编写最少的代码让失败的测试通过。
- 编写最少的代码
- 运行— 赌测试会通过
git gamble --green - 实际运行测试
- 测试失败 → 回滚,尝试其他方案,重复
- 测试通过 → 提交,进入重构阶段
只编写刚好能变绿的代码,不要做多余的优化。
🔵 Refactor Phase — Clean up without changing behaviour
🔵 重构阶段 — 清理代码但不改变行为
Goal: improve code structure while keeping all tests green.
- Rewrite/restructure code (behaviour must stay identical)
- Run — gambles tests will pass
git gamble --refactor - Actually run tests
- Tests fail → revert, try a different refactor, repeat
- Tests pass → commit
- More to refactor? Loop within Refactor
- More features to add? Go back to Red
- Done? Finish
目标:优化代码结构,同时保持所有测试通过。
- 重写/重构代码(行为必须完全不变)
- 运行— 赌测试会通过
git gamble --refactor - 实际运行测试
- 测试失败 → 回滚,尝试其他重构方案,重复
- 测试通过 → 提交
- 还有内容要重构?在重构阶段循环
- 还有功能要添加?回到红阶段
- 完成了?结束
Why the Revert Discipline Matters
为什么回滚规范很重要
TCR alone has a weakness: you never see a test fail, so:
- You might forget an (test always passes vacuously)
assert - You might assert the wrong thing (wrong variable)
TCRDD fixes this: the Red phase requires a failing test before you can commit. If the tests don't fail, the work gets reverted. This forces you to confirm the test actually catches the missing behaviour.
单独使用TCR有一个弱点:你永远看不到测试失败,所以:
- 你可能忘了写(测试永远无意义通过)
assert - 你可能断言了错误的内容(错误的变量)
TCRDD解决了这个问题:红阶段要求你提交前有一个失败的测试。如果测试没有失败,工作就会被回滚。这会迫使你确认测试确实能捕获缺失的功能。
git-gamble Commands
git-gamble 命令
| Command | Phase | What it gambles |
|---|---|---|
| Red | That tests fail |
| Green | That tests pass |
| Refactor | That tests pass |
Under the hood: (TCR), but with the pass/fail expectation flipped for the Red phase.
test && commit || revert| 命令 | 阶段 | 赌的结果 |
|---|---|---|
| 红阶段 | 测试失败 |
| 绿阶段 | 测试通过 |
| 重构阶段 | 测试通过 |
底层逻辑: (TCR),但红阶段的通过/失败预期是反过来的。
test && commit || revertHow to Guide Users Through TCRDD
如何指导用户使用TCRDD
When helping someone practice TCRDD:
-
Identify their current phase. Ask what they just did. Are they about to write a test (Red), about to make it pass (Green), or about to clean up (Refactor)?
-
Coach the correct constraint for that phase.
- Red: "Write only one test. Don't write any implementation yet."
- Green: "Write the minimum code — no more than needed to pass the test."
- Refactor: "Only restructure. If you're adding behaviour, that's a new Red cycle."
-
Remind about the gamble step. Before running tests, the user should declare their expectation with. This is what triggers the automatic commit or revert.
git gamble --<phase> -
When they get a surprise result, help them understand why:
- Unexpected pass in Red → their test didn't capture a real missing behaviour. Revert and rethink the test.
- Unexpected fail in Green/Refactor → their change introduced a regression. Revert and try something smaller.
-
Encourage baby steps. If a user wants to implement a big chunk, help them break it into the smallest possible increment that would change the test outcome.
帮助别人练习TCRDD时:
-
明确他们当前的阶段。 问问他们刚做了什么,他们是准备写测试(红阶段)、准备让测试通过(绿阶段),还是准备清理代码(重构阶段)?
-
指导对应阶段的正确约束。
- 红阶段:“只写一个测试,暂时不要写任何实现代码。”
- 绿阶段:“写最少的代码——不要超过刚好能让测试通过的量。”
- 重构阶段:“只做结构调整,如果你在添加功能,那属于新的红阶段循环。”
-
提醒gamble步骤。 运行测试前,用户应该用声明他们的预期,这才会触发自动提交或回滚。
git gamble --<阶段> -
当他们得到意外结果时,帮他们理解原因:
- 红阶段意外通过 → 他们的测试没有捕获真实的缺失功能,回滚后重新思考测试。
- 绿阶段/重构阶段意外失败 → 他们的改动引入了回归,回滚后尝试更小的改动。
-
鼓励小步迭代。 如果用户想实现一大块功能,帮他们拆分成能改变测试结果的最小增量。
Common Mistakes and How to Address Them
常见错误和解决方法
"I'll write all the tests first, then implement"
→ TCRDD requires one test at a time, one Red-Green-Refactor cycle at a time. This ensures each test has a clear purpose and that you see it fail.
"I added the implementation while writing the test"
→ The test and implementation must be separate commits. Write the test → commit on red → then write implementation → commit on green.
"My refactor changed some behaviour slightly"
→ If tests break, that's a sign behaviour changed. Revert the refactor. Either update the test first (new Red cycle) or find a purer structural refactor.
"The revert erased too much work"
→ This is intentional! It means the step was too big. Take a smaller step next time. This is how TCRDD enforces baby steps.
“我要先写完所有测试,再实现功能”
→ TCRDD要求一次写一个测试,一次走一个红-绿-重构循环。这能保证每个测试都有明确的目的,而且你能看到它失败。
“我写测试的时候顺便把实现也写了”
→ 测试和实现必须是分开的提交。写测试→红阶段提交→然后写实现→绿阶段提交。
“我的重构稍微改了一点行为”
→ 如果测试挂了,说明行为变了。回滚重构,要么先更新测试(新的红阶段循环),要么找更纯粹的结构重构方案。
“回滚删掉了太多工作”
→ 这是有意设计的!说明你这次的步子太大了,下次走小一点。这就是TCRDD强制小步迭代的方式。
Quick Reference Card
快速参考卡
RED → write 1 test → git gamble --red
fail? commit → GREEN
pass? revert → try again
GREEN → write min code → git gamble --green
pass? commit → REFACTOR
fail? revert → try again
REFACTOR → clean code → git gamble --refactor
pass? commit → (loop or done or back to RED)
fail? revert → try againRED → write 1 test → git gamble --red
fail? commit → GREEN
pass? revert → try again
GREEN → write min code → git gamble --green
pass? commit → REFACTOR
fail? revert → try again
REFACTOR → clean code → git gamble --refactor
pass? commit → (loop or done or back to RED)
fail? revert → try againResources
参考资源
- git-gamble theory page — full visual flowcharts for each phase
- git-gamble slides — presentation version of the theory
- TCR original post by Kent Beck
- TDD (Wikipedia)
- git-gamble 理论页面 — 每个阶段的完整可视化流程图
- git-gamble 幻灯片 — 理论的演示版本
- Kent Beck 原创TCR文章
- TDD (维基百科)