test-driven-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese測試驅動開發 (TDD)
Test-Driven Development (TDD)
概述
Overview
先寫測試。看著它失敗。編寫最少的程式碼即可通過。
核心原則: 如果你沒有看到測試失敗,你就不知道它是否測試了正確的東西。
**違反規則的字面意思就是違反規則的精神。 **
Write tests first. Watch them fail. Write the minimal code to make them pass.
Core Principles: If you don't see the test fail, you don't know if it's testing the right thing.
Breaking the letter of the rule means breaking the spirit of the rule.
何時使用
When to Use
總是:
- 新功能
- 錯誤修復
- 重構
- 行為改變
例外(詢問你的人類夥伴):
- 一次性原型
- 生成的代碼
- 配置文件
想「這次跳過 TDD」嗎?停止吧。這就是合理化。
Always:
- New features
- Bug fixes
- Refactoring
- Behavior changes
Exceptions (consult your human partner):
- One-off prototypes
- Generated code
- Configuration files
Thinking "I'll skip TDD this time"? Stop. That's rationalization.
鐵律
Non-Negotiable Rules
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST測試前先寫代碼?刪除它。重新開始。
沒有例外:
- 不要將其保留為“參考”
- 編寫測試時不要“適應”它
- 別看它
- 刪除就是刪除
實施新的測試。時期。
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRSTWrote code before the test? Delete it. Start over.
No exceptions:
- Don't keep it as "reference"
- Don't "adapt" the test to fit the code
- Don't look at it
- Delete means delete
Implement a new test. Period.
紅綠重構
Red-Green-Refactor
dot
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;
}dot
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 - Write a Failing Test
編寫一個最小的測試來顯示應該發生什麼。
<好>
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);
});清晰的名字,測試真實的行為,一件事
</好>
<壞>
typescript
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});模糊的名稱,測試模擬而不是代碼
</壞>
要求:
- 一種行為
- 清晰的名字
- 真實程式碼(除非不可避免,否則不進行模擬)
Write a minimal test that shows what should happen.
<Good>
```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);
});
Clear name, tests real behavior, focuses on one single thing
</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);
});Vague name, tests mocks instead of actual code
</Bad>
Requirements:
- One behavior per test
- Clear name
- Real code (avoid mocks unless unavoidable)
驗證紅色 - 觀察失敗
Verify RED - Watch It Fail
**強制的。切勿跳過。 **
bash
npm test path/to/test.test.ts確認:
- 測試失敗(不是錯誤)
- 預計會出現失敗訊息
- 由於功能缺失(不是拼寫錯誤)而失敗
**測試通過? ** 您正在測試現有行為。修復測試。
**測試錯誤? ** 修復錯誤,重新運行,直到正確失敗。
Mandatory. Never skip.
bash
npm test path/to/test.test.tsConfirm:
- The test fails (not errors out)
- The failure message is as expected
- It fails due to missing functionality (not typos)
Test passed? You're testing existing behavior. Fix the test.
Test errored? Fix the error, re-run until it fails correctly.
綠色 - 最少代碼
GREEN - Minimal Code
編寫最簡單的程式碼來通過測試。
<好>
typescript
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');
}只要夠通過即可
</好>
<壞>
typescript
async function retryOperation<T>(
fn: () => Promise<T>,
options?: {
maxRetries?: number;
backoff?: 'linear' | 'exponential';
onRetry?: (attempt: number) => void;
}
): Promise<T> {
// YAGNI
}過度設計
</壞>
不要添加功能、重構其他程式碼或在測試之外進行「改進」。
Write the simplest code possible to pass the test.
<Good>
```typescript
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 the test
</Good>
<Bad>
```typescript
async function retryOperation<T>(
fn: () => Promise<T>,
options?: {
maxRetries?: number;
backoff?: 'linear' | 'exponential';
onRetry?: (attempt: number) => void;
}
): Promise<T> {
// YAGNI
}
```
Over-engineered
</Bad>
Don't add extra features, refactor other code, or make "improvements" beyond what the test requires.
驗證綠色 - 觀察它通過
Verify GREEN - Watch It Pass
**強制的。 **
bash
npm test path/to/test.test.ts確認:
- 測試通過
- 其他測試仍然通過
- 輸出原始(沒有錯誤、警告)
**測試失敗? ** 修復代碼,而不是測試。
**其他測試失敗? ** 立即修復。
Mandatory.
bash
npm test path/to/test.test.tsConfirm:
- The test passes
- All other tests still pass
- Output is clean (no errors, warnings)
Test failed? Fix the code, not the test.
Other tests failed? Fix them immediately.
重構 - 清理
REFACTOR - Clean Up
僅綠色之後:
- 刪除重複項
- 改進名字
- 提取助手
保持測試綠色。不要添加行為。
Only when all tests are green:
- Remove duplication
- Improve naming
- Extract helper functions
Keep tests green. Don't add new behavior.
重複
Repeat
下一個功能的下一個失敗測試。
Write the next failing test for the next feature.
良好的測試
Good Tests
| 品質 | 好 | 不好 |
|---|---|---|
| 最小 | 一件事。名字中的“和”?分開它。 | |
| 清除 | 名稱描述行為 | |
| 表明意圖 | 所需需要的API | 模糊了程序代碼執行 |
| Quality | Good | Bad |
|---|---|---|
| Minimal | One thing. "And" in the name? Split it. | |
| Clear | Name describes behavior | |
| Expresses Intent | Shows the desired API | Obscures how the code executes |
為什麼訂單很重要
Why Order Matters
“我會在之後編寫測試來驗證它是否有效”
程式碼通過後編寫的測試立即通過。立即通過並不能證明什麼:
- 可能測試錯誤的東西
- 可能測試實施,而不是行為
- 可能會錯過您忘記的邊緣情況
- 你從未遇到過它捕獲 bug
測試優先迫使您看到測試失敗,證明它確實測試了某些東西。
「我已經手動測試了所有邊緣情況」
手動測試是臨時的。您認為您測試了所有內容,但:
- 沒有記錄您測試的內容
- 代碼更改後無法重新運行
- 在壓力下容易忘記案件
- “我嘗試了一下就成功了”≠全面
自動化測試是系統化的。他們每次都以同樣的方式奔跑。
「消除X小時的工作就是浪費」
沉沒成本謬誤。時間已經過去了。您現在的選擇:
- 使用TDD刪除並重寫(多花幾個小時,高可信度)
- 保留它並在之後添加測試(30 分鐘,低置信度,可能存在錯誤)
“浪費”是保留你不信任的代碼。沒有真正測試的工作代碼是技術債。
「TDD 是教條主義的,務實意味著適應」
TDD 很務實:
- 在提交之前發現錯誤(比之後調試更快)
- 防止回歸(測試立即捕獲中斷)
- 文檔行為(測試顯示如何使用程式碼)
- 啟用重構(自由更改,測試捕獲中斷)
“實用”快捷方式=生產中的調試=速度較慢。
「達到相同目標後進行測試 - 這是精神而不是儀式」
不。回答後測試“這有什麼作用?”測試優先回答“這應該做什麼?”
之後的測試因您的實現而存在偏差。您測試您構建的內容,而不是測試所需的內容。您驗證記住的邊緣情況,而不是發現的情況。
測試優先強制在實施之前發現邊緣情況。測試 - 驗證您記住了所有內容(您沒有記住)。
≠ TDD 經過 30 分鐘的測試。你得到了保險,失去了證明測試的工作。
"I'll write tests later to verify it works"
Tests written after code pass immediately. Immediate passing proves nothing:
- It might test the wrong thing
- It might test the implementation, not the behavior
- It might miss edge cases you forgot
- It will never catch bugs you didn't anticipate
Test-first forces you to see the test fail, proving it actually tests something.
"I've manually tested all edge cases"
Manual testing is ad-hoc. You think you tested everything, but:
- There's no record of what you tested
- You can't re-run it after code changes
- It's easy to forget cases under pressure
- "I tried it once and it worked" ≠ comprehensive
Automated tests are systematic. They run the same way every time.
"Deleting X hours of work is a waste"
Sunk cost fallacy. The time is already spent. Your options now:
- Delete and rewrite with TDD (a few more hours, high confidence)
- Keep it and add tests later (30 minutes, low confidence, potential bugs)
The "waste" is keeping code you don't trust. Working code without real tests is technical debt.
"TDD is dogmatic, pragmatism means adapting"
TDD is pragmatic:
- Catches bugs before they're committed (faster than debugging later)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use the code)
- Enables refactoring (change freely, tests catch breaks)
"Pragmatic" shortcuts = debugging in production = slower overall.
"Testing after achieves the same goal - it's the spirit not the ritual"
No. Tests-after answer "What does this do?" Test-first answers "What should this do?"
Tests-after are biased by your implementation. You test what you built, not what you needed. You verify edge cases you remembered, not the ones you missed.
Test-first forces you to discover edge cases before implementation. Tests-after only verify what you remembered (and you didn't remember everything).
≠ TDD with tests written 30 minutes later. You get the insurance, but lose the proof that the tests work.
常見的合理化理由
Common Rationalizations
| 對不起 | 現實 |
|---|---|
| “太簡單了,無法測試” | 簡單的代碼中斷。測試需要 30 秒。 |
| “之後我會測試” | 測試立即通過並不能證明什麼。 |
| “達到相同目標後再進行測試” | Tests-after =“這是做什麼的?”測試優先=“這應該做什麼?” |
| “已經手動測試” | 臨時性≠系統性。沒有記錄,無法重新運行。 |
| “刪除X小時是浪費” | 沉沒成本謬誤。保留默認驗證的代碼是技術債。 |
| 「留作參考,先寫測驗」 | 你會適應它。後面就是測試了刪除就是刪除的意思。 |
| “需要先探索” | 美好的。放棄探索,從TDD開始。 |
| “努力測試=設計不清楚” | 聽聽測試。難以測試=難以使用。 |
| “TDD 會讓我放慢速度” | TDD 比調試更快。務實=測試第一。 |
| “手動測試速度更快” | 手冊不能證明邊緣情況。您將重新測試每個變更。 |
| “現有代碼沒有經過測試” | 你正在改進它。為現有代碼添加測試。 |
| Excuse | Reality |
|---|---|
| "It's too simple to test" | Simple code breaks. Writing the test takes 30 seconds. |
| "I'll test later" | Tests written later pass immediately and prove nothing. |
| "Testing after achieves the same goal" | Tests-after = "What does this do?" Test-first = "What should this do?" |
| "I already tested manually" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is a waste" | Sunk cost fallacy. Keeping unvalidated code is technical debt. |
| "I'll keep this as reference and write the test first next time" | You'll adapt the test to fit it. Later becomes never. Delete means delete. |
| "I need to explore first" | Fine. Do your exploration, then start with TDD. |
| "Hard to test = unclear design" | Listen to the tests. Hard to test = hard to use. |
| "TDD slows me down" | TDD is faster than debugging. Pragmatism = test first. |
| "Manual testing is faster" | Manual can't prove edge cases. You'll have to re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
危險信號 - 停止並重新開始
Red Flags - Stop and Start Over
- 測試前的代碼
- 實施後測試
- 測試立即通過
- 無法解釋測試失敗的原因
- “稍後”添加測試
- 合理化“就這一次”
- “我已經手動測試過了”
- “達到相同目的後進行測試”
- “這是關於精神而不是儀式”
- “保留作為參考”或“改編現有代碼”
- “已經花了X個小時了,刪掉太浪費了”
- “TDD很教條,我很務實”
- “這是不同的,因為……”
**所有這些意味著:刪除計劃碼。從TDD開始。 **
- Wrote code before the test
- Wrote tests after implementation
- Tests pass immediately
- Can't explain why a test failed
- Planning to add tests "later"
- Rationalizing "just this once"
- "I already tested manually"
- "Testing after achieves the same goal"
- "It's about the spirit not the ritual"
- "Keeping it as reference" or "adapting existing code"
- "I spent X hours, deleting is a waste"
- "TDD is dogmatic, I'm pragmatic"
- "This is different because..."
All of these mean: Delete the production code. Start over with TDD.
示例:錯誤修復
Example: Bug Fix
錯誤: 接受空電子郵件
紅色的
typescript
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});驗證紅色
bash
$ npm test
FAIL: expected 'Email required', got undefined綠色的
typescript
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}驗證綠色
bash
$ npm test
PASS重構
如果需要,提取多個欄位的驗證。
Bug: Accepts empty email addresses
RED
typescript
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});Verify RED
bash
$ npm test
FAIL: expected 'Email required', got undefinedGREEN
typescript
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}Verify GREEN
bash
$ npm test
PASSREFACTOR
Extract validation for multiple fields if needed.
驗證清單
Verification Checklist
在標記工作完成之前:
- 每個新函數/方法都有一個測試
- 在實施之前觀察每個測試的失敗
- 每個測驗都因預期原因而失敗(功能缺失,而非拼字錯誤)
- 編寫最少的代碼來通過每個測試
- 所有測試均通過
- 輸出原始(沒有錯誤、警告)
- 測試使用真實程式碼(僅在不可避免時才進行模擬)
- 涵蓋的邊緣情況和錯誤
無法完成所有中斷嗎?你跳過了 TDD。重新開始。
Before marking work complete:
- Every new function/method has a test
- Watched each test fail before implementation
- Each test failed for the expected reason (missing functionality, not typos)
- Wrote minimal code to pass each test
- All tests pass
- Output is clean (no errors, warnings)
- Tests use real code (mocks only when unavoidable)
- Edge cases and errors are covered
Can't check all boxes? You skipped TDD. Start over.
卡住時
When Stuck
| 問題 | 解決方案 |
|---|---|
| 不知道如何測試 | 編寫想要的API。先寫斷言。詢問你的人類夥伴。 |
| 測試太複雜 | 設計太複雜了。簡化界面。 |
| 必須嘲笑一切 | 代碼耦合性太強。使用依賴注入。 |
| 測試設定巨大 | 提取助手。還是很複雜?簡化設計。 |
| Problem | Solution |
|---|---|
| Don't know how to test | Write the API you want. Write the assertions first. Consult your human partner. |
| Test is too complex | Design is too complex. Simplify the interface. |
| Have to mock everything | Code is too coupled. Use dependency injection. |
| Test setup is huge | Extract helpers. Still complex? Simplify the design. |
偵錯集成
Debugging Integration
發現錯誤了嗎?寫一個失敗的測試來重置它。遵循TDD週期。測試證明可以修復並阻止回歸。
未經測試切勿修復錯誤。
Found a bug? Write a failing test to reproduce it. Follow the TDD cycle. The test proves the fix works and prevents regressions.
Never fix a bug without writing a test first.
測試反模式
Testing Anti-Patterns
在新增模擬或測試實用程式時,請閱讀@testing-anti-patterns.md分區常見陷阱:
- 測試模擬行為而不是真實行為
- 將僅測試方法添加到生產類中
- 在不瞭解依賴關係的情況下進行模擬
When adding mocks or test utilities, refer to @testing-anti-patterns.md for common pitfalls:
- Testing mock behavior instead of real behavior
- Adding test-only methods to production classes
- Mocking without understanding dependencies
最終規則
Final Rule
Production code → test exists and failed first
Otherwise → not TDD未經您的人類伴侶許可,也不例外。
Production code → test exists and failed first
Otherwise → not TDDNo exceptions without approval from your human partner.