🎯 什麼情境該想到我
當你想實作一個功能、又想讓它天然可測、乾淨、不易出錯時。
⚙️ 怎麼用(小步循環)
- 🔴 紅:先寫一個描述「你要的行為」的測試 —— 它現在會失敗(因為還沒實作)。
- 🟢 綠:用最快、最簡單的方式讓測試通過(可以先寫死、先醜,重點是綠燈)。
- ♻️ 重構:在綠燈保護下,去除重複、整理結構(見 工具-提煉函式)——每步都保持綠。
- 回到步驟 1,加下一個測試。步子小到幾乎不可能出錯;出錯就把步子切更小。
★ 從「測試清單」開始,而不是從第一個測試開始
TDD 的第一步其實是寫一份清單:把你想到的所有行為、邊界案例列下來。然後只挑一個轉成測試。做的過程中想到新案例,加到清單上,不要馬上去做——這讓你一次只專注一件事。
□ 空購物車總額為 0
□ 一件商品:總額 = 單價 × 數量
□ 多件商品加總
□ 折扣碼 10% off
□ 折扣後金額四捨五入到元 ← 做到一半想到的,先記下
□ 數量為負數要回錯誤
挑選順序:先挑你最有把握、最能推進設計的,不一定是最簡單的。
★ 綠燈的三種策略(依把握程度選)
| 策略 | 什麼時候用 | 做法 |
|---|---|---|
| 顯而易見的實作(Obvious Implementation) | 你確定怎麼寫 | 直接寫出正確實作 |
| 假裝(Fake It) | 不確定,想先讓紅燈變綠 | return 0——寫死期望值,然後在重構階段一步步把常數換成變數 |
| 三角定位(Triangulate) | 不知道怎麼一般化 | 再加一個不同輸入的測試,逼出一般化的實作(兩個例子才看得出規律) |
原則:有把握時大步走(顯而易見的實作);一旦出現意外的紅燈,立刻換小步(假裝 + 三角定位)。
★ 在 Go 裡跑一輪
// 🔴 1. 先寫測試。Total 還不存在 → 編譯失敗,這也算紅燈
func TestTotal(t *testing.T) {
tests := []struct {
name string
items []Item
want int
}{
{"empty", nil, 0},
{"one item", []Item{{Price: 100, Qty: 2}}, 200},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Total(tt.items); got != tt.want {
t.Errorf("Total() = %d, want %d", got, tt.want)
}
})
}
}
// 🔴 2. 讓它編譯:寫一個回傳零值的空殼 → 現在是「斷言失敗」的紅燈,
// 確認測試真的會因為對的理由失敗("one item" 失敗、"empty" 通過)
func Total(items []Item) int { return 0 }
// 🟢 3. 最簡單的實作
func Total(items []Item) int {
sum := 0
for _, it := range items {
sum += it.Price * it.Qty
}
return sum
}
// ♻️ 4. 重構(此例已經夠簡單,跳過)→ 回到清單挑下一個第 2 步不要省:看到測試「因為正確的理由」失敗,才能確認這個測試真的有在檢查東西。一個從來沒紅過的測試,可能永遠不會紅。
Go 的表格測試讓「加下一個案例」只是加一行,特別適合三角定位。表格測試的完整寫法見 工具-Go表格測試與子測試。
★ 一輪該多長
- 一輪紅綠重構:幾分鐘。超過 10 分鐘還在綠燈的路上,代表這一步太大——退回,挑一個更小的測試。
- 在 Go 裡用
go test ./pkg/ -run TestTotal只跑目前這個測試,回饋在一秒內;編輯器的「儲存時執行測試」可以再省一步。
🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意
-
別跳過重構那步(只紅綠不重構=債越積越多);也別在紅燈時做重構。
-
不要在紅燈時重構:紅燈時你同時有「行為不對」和「結構改變」兩個變因,出錯無法分辨。先綠,再重構。
-
TDD 不適合所有工作:探索性的原型(還不知道要做什麼)、純 UI 排版、和外部系統的串接實驗,先做 spike 再回頭補測試通常更有效率;spike 的程式碼要丟掉重寫,而不是直接上線。
🔗 相關工具
- 工具-測試先行的設計 —— 為什麼要這樣跑,這張講節奏,它講先寫測試帶來的設計效果
- 工具-小步重構 —— 第三步 Refactor 的操作細節,在綠燈保護下一次改一小塊
- 工具-提煉函式 —— Refactor 階段最常用的那一招