📌 30 秒摘要(Layer 3)

TDD 把順序倒過來:先寫一個會失敗的測試,再寫剛好讓它通過的程式,然後重構——紅、綠、重構,小步循環。它不只是測試手段,更是一種設計方法:被測試逼著寫的程式碼,天然是低耦合、可測、介面清楚的。核心心法:永遠讓程式碼在「可運作」與「乾淨」兩個狀態間小步移動,隨時都有綠燈保護。

🗺 心智圖(Canvas)

測試驅動開發

測試驅動開發

Test-Driven Development — Kent Beck

🔁 紅 → 綠 → 重構

🎯 什麼情境該想到我

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

⚙️ 怎麼用(小步循環)

  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 的程式碼要丟掉重寫,而不是直接上線。

🔗 相關工具

🔴 紅 先寫一個描述「要什麼行為」的測試,它現在會失敗

🟢 綠 用最快、最簡單的方式讓它通過(可以先醜、先寫死)

♻️ 重構 在綠燈保護下去除重複、整理結構,每步都保持綠

👣 小步前進

每步小到「幾乎不可能出錯」 隨時都有綠燈保護,出錯範圍極小

不確定就把步子切更小 步伐大小是可調節的旋鈕,不是固定值

「先讓它動,再讓它對,最後讓它快。」 程式碼在「可運作」與「乾淨」兩態間小步移動

🟩 讓測試變綠的策略

假實作 Fake It 先直接寫死回傳值讓燈變綠,再逐步一般化

三角測量 Triangulation 加第二、第三個例子,逼出真正的通用解

顯明實作 Obvious Implementation 答案很明顯時就直接寫對,別繞路

🎨 TDD 是設計工具

🎯 什麼情境該想到我

當你的程式「很難寫測試」(依賴一堆、要 mock 一大票),或想讓設計更清楚時。難測,往往是設計不良的訊號。

⚙️ 怎麼用

  1. 先寫測試 = 先從「使用者角度」設計介面:你被迫先想「這東西怎麼被呼叫最順」,介面自然變好用。
  2. 難測就是設計警報:如果要測某段得建構一堆相依 → 代表耦合太緊,該解耦(見 工具-打破依賴以便測試、工具-針對介面編程)。
  3. 讓測試逼出低耦合:依賴用注入、對介面編程,程式自然可測、可替換。
  4. 測試即活文件:讀測試就知道這段程式該有什麼行為。

★ 「難測」是哪一種設計問題:對照表

測試寫起來痛苦時,痛的位置會指出設計問題的種類:

測試時的痛對應的設計問題改法
要 mock 5 個以上的依賴這個單元職責太多拆成幾個各自依賴較少的單元
想測私有函式私有函式裡藏了另一個概念把它提煉成獨立的型別或套件,變成公開的東西來測
準備資料要 50 行物件建構太複雜或依賴太深建構函式只收真正需要的東西;用 builder 或測試用的工廠函式
結果取決於現在時間、亂數隱藏的依賴(全域的時鐘、亂數來源)把時鐘、亂數當參數注入
要真的連 DB 才能測業務規則業務邏輯和 I/O 糾纏把純邏輯拉出來成純函式,I/O 留在外層(「功能核心、命令外殼」)
斷言要檢查內部欄位行為沒有可觀察的輸出讓函式回傳結果,而不是只改內部狀態

用法:遇到難測的程式碼,不要先找更強大的 mock 工具——先查這張表,看是不是該改設計。

★ Go 讓「可測性」自然出現的三個手法

1. 在使用端定義小介面

// 在 billing 套件(使用端)定義,只放用到的方法
type invoiceStore interface {
	OpenInvoices(ctx context.Context, month time.Time) ([]Invoice, error)
}
 
type Closer struct{ store invoiceStore }

生產環境傳進真的 *postgres.Repo(它有 30 個方法,但隱式地滿足了這個 1 個方法的介面);測試傳一個 5 行的假實作。不需要 mock 框架。詳見 工具-Go介面小而隱式。

2. 時間當參數注入

type Clock func() time.Time
 
func IsExpired(now Clock, exp time.Time) bool { return now().After(exp) }
// 測試:IsExpired(func() time.Time { return fixed }, exp)

3. 需要真的等待時間的併發程式碼:testing/synctest(Go 1.25+)

synctest.Test 在一個隔離的「泡泡」裡執行測試,泡泡裡的 time 套件使用假時鐘,只有在泡泡內所有 goroutine 都阻塞時才前進(依 Go 1.26.4 套件文件,2026-09-25)。所以一個 time.Sleep(2 * time.Second) 的測試會立即完成,而且結果是決定性的——不用為了可測性把程式碼改寫成注入時鐘的形狀。

★ 兩個學派:該 mock 到什麼程度

古典學派(Detroit/Chicago)Mock 學派(London)
測試單位一個行為,可以跨好幾個真實物件一個類別,所有協作者都 mock
替身用在哪只替換慢的或不可控的(DB、網路、時鐘)所有協作者
優點重構時測試不會壞設計介面時強迫你想清楚協作關係
缺點失敗時比較難定位是哪個物件測試綁住實作細節,重構時一堆測試跟著壞

Go 社群多數偏向古典學派:用真實的物件,只在系統邊界(DB、HTTP、時間)換成假實作。如果你每次重構都在改測試,可能是 mock 得太多了(見 工具-小步重構「測試不該跟著改」)。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意

  • 目標是「設計驅動」,不是「為了 100% 覆蓋率而測」;測試是設計的副產品與保護網。

  • 為了可測性而加的介面,只有一個實作時要自問:這個介面是在「系統邊界」(值得)還是在「兩個內部 struct 之間」(常常只是為了 mock,徒增間接層)?

🔗 相關工具

先寫測試=從使用端設計介面 被迫先想「這東西怎麼被呼叫最順」

難測就是設計警報 要 mock 一大票 → 耦合太緊,該解耦

測試即規格與活文件 讀測試就知道這段程式該有什麼行為

Link to original

🧰 這本書給我的工具

✨ 關鍵重點(Layer 1–2)

  • 紅 → 綠 → 重構:先讓測試失敗(紅)、用最快方式讓它通過(綠)、再重構去除重複(保持綠)。
  • 小步前進:每步小到「幾乎不可能出錯」;不確定就把步子再切小。
  • 測試即規格:測試先寫,等於先想清楚「要什麼行為」。
  • TDD 是設計工具:難測的程式通常是設計不良的訊號 → 逼你解耦。
  • 兩種讓綠燈的策略:假實作(先寫死)、三角測量(多個例子逼出通用解)。

💬 金句原文(Layer 0)

  • 「先讓它動,再讓它對,最後讓它快。」

🔗 相關