🎯 什麼情境該想到我

當你想實作一個功能、又想讓它天然可測、乾淨、不易出錯時。

⚙️ 怎麼用(小步循環)

  1. 🔴 紅:先寫一個描述「你要的行為」的測試 —— 它現在會失敗(因為還沒實作)。
  2. 🟢 綠:用最快、最簡單的方式讓測試通過(可以先寫死、先醜,重點是綠燈)。
  3. ♻️ 重構:在綠燈保護下,去除重複、整理結構(見 工具-提煉函式)——每步都保持綠。
  4. 回到步驟 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 的程式碼要丟掉重寫,而不是直接上線。

🔗 相關工具