| name | test-driven-development |
| description (EN) | Use when implementing any feature or bugfix, before writing implementation code |
| 說明 (繁中) | 使用時機:實作任何功能或修 bug 時,在寫實作程式碼之前 |
Write the test first. Watch it fail. Write minimal code to pass.
先寫測試。看著它失敗。寫最少的程式碼讓它通過。
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
核心原則: 如果你沒親眼看到測試失敗,你就不知道它測試的是不是對的東西。
Violating the letter of the rules is violating the spirit of the rules.
違反規則的字面,就是違反規則的精神。
Always: - New features - Bug fixes - Refactoring - Behavior changes
一律使用: - 新功能 - 修 bug - 重構 - 行為變更
Exceptions (ask your human partner): - Throwaway prototypes - Generated code - Configuration files
例外(問你的真人夥伴): - 用完即丟的原型 - 產生的程式碼 - 設定檔
Thinking "skip TDD just this once"? Stop. That's rationalization.
心裡想「這次就跳過 TDD 吧」?停。那就是合理化藉口。
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
沒有先寫失敗的測試,就不准有正式程式碼
Write code before the test? Delete it. Start over.
在測試之前先寫了程式碼?刪掉。重新開始。
No exceptions: - Don't keep it as "reference" - Don't "adapt" it while writing tests - Don't look at it - Delete means delete
沒有例外: - 不要把它留著當「參考」 - 不要邊寫測試邊「改寫」它 - 不要看它 - 刪掉就是刪掉
Implement fresh from tests. Period.
從測試開始全新實作。就這樣。
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;
}
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;
}
Write one minimal test showing what should happen.
寫一支最小的測試,顯示應該發生什麼事。
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);
});
Clear name, tests real behavior, one thing
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);
});
名稱清楚、測試真實行為、一次只測一件事
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
Vague name, tests mock not code
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
名稱含糊、測試的是 mock 而不是程式碼
Requirements: - One behavior - Clear name - Real code (no mocks unless unavoidable)
需求: - 一個行為 - 名稱清楚 - 真實程式碼(除非無可避免,否則不用 mock)
MANDATORY. Never skip.
強制。絕不可跳過。
npm test path/to/test.test.ts
npm test path/to/test.test.ts
Confirm: - Test fails (not errors) - Failure message is expected - Fails because feature missing (not typos)
確認: - 測試失敗(不是出錯) - 失敗訊息符合預期 - 因為功能缺失而失敗(不是打錯字)
Test passes? You're testing existing behavior. Fix test.
測試通過了? 你在測試既有的行為。修正測試。
Test errors? Fix error, re-run until it fails correctly.
測試出錯了? 修正錯誤,重新執行直到它正確地失敗。
Write simplest code to pass the test.
寫最簡單的程式碼讓測試通過。
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');
}
Just enough to pass
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
}
Over-engineered
async function retryOperation<T>(
fn: () => Promise<T>,
options?: {
maxRetries?: number;
backoff?: 'linear' | 'exponential';
onRetry?: (attempt: number) => void;
}
): Promise<T> {
// YAGNI
}
過度設計
Don't add features, refactor other code, or "improve" beyond the test.
不要加功能、不要重構其他程式碼、不要「改良」到測試以外的範圍。
MANDATORY.
強制。
npm test path/to/test.test.ts
npm test path/to/test.test.ts
Confirm: - Test passes - Other tests still pass - Output pristine (no errors, warnings)
確認: - 測試通過 - 其他測試仍然通過 - 輸出乾淨(沒有錯誤、警告)
Test fails? Fix code, not test.
測試失敗了? 修程式碼,不是修測試。
Other tests fail? Fix now.
其他測試失敗? 現在就修。
After green only: - Remove duplication - Improve names - Extract helpers
只在變綠之後: - 移除重複 - 改善命名 - 抽取輔助函式
Keep tests green. Don't add behavior.
保持測試變綠。不要加行為。
Next failing test for next feature.
為下一個功能寫下一支失敗的測試。
| Quality | Good | Bad |
|---|---|---|
| Minimal | One thing. "and" in name? Split it. | test('validates email and domain and whitespace') |
| Clear | Name describes behavior | test('test1') |
| Shows intent | Demonstrates desired API | Obscures what code should do |
| 品質 | 好 | 壞 |
|---|---|---|
| 最小 | 一件事。名稱裡有「and」?拆開它。 | test('validates email and domain and whitespace') |
| 清楚 | 名稱描述行為 | test('test1') |
| 顯示意圖 | 示範想要的 API | 模糊了程式碼該做什麼 |
When writing or changing any test, read writing-good-tests.md for the rules that keep tests honest: - Name the production change that would make the test fail — before writing it - Assert on real behavior, never on mock behavior - Keep test-only code in test utilities, out of production classes - Understand a dependency's side effects before mocking it
寫或修改任何測試時,閱讀 writing-good-tests.md 中讓測試保持誠實的規則: - 在寫之前,說出會讓測試失敗的正式程式碼變更——在寫它之前 - 斷言真實行為,絕不斷言 mock 行為 - 把僅供測試的程式碼放在測試工具中,別放進正式類別 - 在模擬相依物件之前,先了解它的副作用
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests written after pass immediately — which proves nothing. They may test the wrong thing, test the implementation instead of the behavior, or miss the edge case you forgot. You never watched it fail, so you never proved it can catch the bug. Test-first forces that failure. |
| "Tests after achieve same goals (spirit not ritual)" | Tests-after answer "what does this do?"; tests-first answer "what should this do?" Tests written after are biased by the code you already wrote — you verify the cases you remembered, not the ones you'd have discovered. Coverage without proof the tests work. |
| "Already manually tested" | Manual testing is ad-hoc: no record of what you covered, no way to re-run it when the code changes, easy to forget cases under pressure. "Worked when I tried it" ≠ comprehensive. Automated tests run the same way every time. |
| "Deleting X hours is wasteful" | Sunk cost fallacy — that time is already spent either way. The real choice: rewrite with TDD (high confidence) vs. keep it and bolt tests on after (low confidence, likely bugs). Keeping code you can't trust is the waste. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD IS the pragmatic path: catches bugs before commit, prevents regressions, lets you refactor without fear. "Pragmatic" shortcuts mean debugging in production — slower, not faster. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
| 藉口 | 事實 |
|---|---|
| 「太簡單了,不用測」 | 簡單的程式碼也會壞。測試只要 30 秒。 |
| 「我之後再測」 | 事後寫的測試會立刻通過——這證明不了任何事。它們可能測錯了東西、測了實作而不是行為,或漏掉了你忘記的邊緣案例。你從沒看過它失敗,所以從沒證明它抓得到 bug。測試優先強迫你面對那個失敗。 |
| 「事後測試也達到同樣目標(精神而非儀式)」 | 事後測試回答的是「這程式碼做什麼?」;測試優先回答的是「這程式碼應該做什麼?」。事後寫的測試被你已經寫的程式碼偏誤——你驗證的是你記得的案例,而不是你會發現的那些。覆蓋率卻沒有證明測試有效。 |
| 「已經手動測過了」 | 手動測試是 ad-hoc:沒有你覆蓋了什麼的紀錄、程式碼變更時無法重跑、壓力下容易忘記案例。「我試過可以用」≠ 全面。自動化測試每次都用相同方式跑。 |
| 「刪掉 X 小時的心血太浪費」 | 沉沒成本謬誤——無論如何那時間都花掉了。真正的選擇是:用 TDD 重寫(高信心)對上保留它然後事後硬補測試(低信心、很可能有 bug)。保留你無法信任的程式碼才是浪費。 |
| 「留著當參考,然後測試優先」 | 你會去改它的。那就是事後測試。刪掉就是刪掉。 |
| 「需要先探索」 | 可以。丟掉探索的產物,用 TDD 重新開始。 |
| 「測試難寫=設計不清楚」 | 傾聽測試。難測=難用。 |
| 「TDD 會拖慢我」 | TDD 本身就是務實的路:在 commit 前抓出 bug、防止迴歸、讓你能無所畏懼地重構。「務實」的捷徑意味著在正式環境除錯——更慢,不是更快。 |
| 「手動測試更快」 | 手動測試無法證明邊緣案例。你每次變更都要重測。 |
| 「既有程式碼沒有測試」 | 你正在改善它。為既有程式碼補測試。 |
All of these mean: Delete code. Start over with TDD.
所有這些都代表:刪掉程式碼。用 TDD 重新開始。
Bug: Empty email accepted
Bug: 空 email 被接受
RED
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
紅
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
Verify RED
$ npm test
FAIL: expected 'Email required', got undefined
驗證紅
$ npm test
FAIL: expected 'Email required', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
綠
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
Verify GREEN
$ npm test
PASS
驗證綠
$ npm test
PASS
REFACTOR Extract validation for multiple fields if needed.
重構 如有需要,抽取多欄位的驗證邏輯。
Before marking work complete:
在標記工作完成之前:
Can't check all boxes? You skipped TDD. Start over.
勾不完?你跳過了 TDD。重新開始。
| Problem | Solution |
|---|---|
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
| Test too complicated | Design too complicated. Simplify interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
| 問題 | 解法 |
|---|---|
| 不知道怎麼測 | 寫你想要的 API。先寫斷言。問你的真人夥伴。 |
| 測試太複雜 | 設計太複雜。簡化介面。 |
| 什麼都要 mock | 程式碼太耦合。用依賴注入。 |
| 測試設定很龐大 | 抽取輔助函式。還是複雜?簡化設計。 |
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
發現 bug?寫一支重現它的失敗測試。遵循 TDD 循環。測試證明修復並防止迴歸。
Never fix bugs without a test.
絕不沒有測試就修 bug。
Production code → test exists and failed first
Otherwise → not TDD
正式程式碼 → 測試存在且先失敗過
否則 → 不是 TDD
No exceptions without your human partner's permission.
未經你的真人夥伴允許,沒有例外。