📌 30 秒摘要(Layer 3)
TDD 把順序倒過來:先寫一個會失敗的測試,再寫剛好讓它通過的程式,然後重構——紅、綠、重構,小步循環。它不只是測試手段,更是一種設計方法:被測試逼著寫的程式碼,天然是低耦合、可測、介面清楚的。核心心法:永遠讓程式碼在「可運作」與「乾淨」兩個狀態間小步移動,隨時都有綠燈保護。
🗺 心智圖(Canvas)
測試驅動開發
Link to original 測試驅動開發
Test-Driven Development — Kent Beck
🔁 紅 → 綠 → 重構
🎯 什麼情境該想到我
當你想實作一個功能、又想讓它天然可測、乾淨、不易出錯時。
⚙️ 怎麼用(小步循環)
- 🔴 紅:先寫一個描述「你要的行為」的測試 —— 它現在會失敗(因為還沒實作)。
- 🟢 綠:用最快、最簡單的方式讓測試通過(可以先寫死、先醜,重點是綠燈)。
- ♻️ 重構:在綠燈保護下,去除重複、整理結構(見 工具-提煉函式)——每步都保持綠。
- 回到步驟 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 階段最常用的那一招
🔴 紅 先寫一個描述「要什麼行為」的測試,它現在會失敗
🟢 綠 用最快、最簡單的方式讓它通過(可以先醜、先寫死)
♻️ 重構 在綠燈保護下去除重複、整理結構,每步都保持綠
👣 小步前進
每步小到「幾乎不可能出錯」 隨時都有綠燈保護,出錯範圍極小
不確定就把步子切更小 步伐大小是可調節的旋鈕,不是固定值
「先讓它動,再讓它對,最後讓它快。」 程式碼在「可運作」與「乾淨」兩態間小步移動
🟩 讓測試變綠的策略
假實作 Fake It 先直接寫死回傳值讓燈變綠,再逐步一般化
三角測量 Triangulation 加第二、第三個例子,逼出真正的通用解
顯明實作 Obvious Implementation 答案很明顯時就直接寫對,別繞路
🎨 TDD 是設計工具
🎯 什麼情境該想到我
當你的程式「很難寫測試」(依賴一堆、要 mock 一大票),或想讓設計更清楚時。難測,往往是設計不良的訊號。
⚙️ 怎麼用
- 先寫測試 = 先從「使用者角度」設計介面:你被迫先想「這東西怎麼被呼叫最順」,介面自然變好用。
- 難測就是設計警報:如果要測某段得建構一堆相依 → 代表耦合太緊,該解耦(見 工具-打破依賴以便測試、工具-針對介面編程)。
- 讓測試逼出低耦合:依賴用注入、對介面編程,程式自然可測、可替換。
- 測試即活文件:讀測試就知道這段程式該有什麼行為。
★ 「難測」是哪一種設計問題:對照表
測試寫起來痛苦時,痛的位置會指出設計問題的種類:
測試時的痛 對應的設計問題 改法 要 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,徒增間接層)?
🔗 相關工具
- 工具-紅綠重構循環 —— 節奏卡,這張講「為什麼先寫測試」,它講「一輪怎麼跑」
- 工具-打破依賴以便測試 —— 面對舊碼時的補救手法,先行做不到就只能事後拆依賴
- 工具-針對介面編程 —— 讓測試好寫的設計手段,依賴介面才能替換成測試替身
- 工具-Go表格測試與子測試 —— 測試寫出來之後的結構:表格驅動、子測試、失敗訊息
先寫測試=從使用端設計介面 被迫先想「這東西怎麼被呼叫最順」
難測就是設計警報 要 mock 一大票 → 耦合太緊,該解耦
測試即規格與活文件 讀測試就知道這段程式該有什麼行為
🧰 這本書給我的工具
- 工具-紅綠重構循環 — 想用測試驅動出乾淨程式時
- 工具-測試先行的設計 — 想讓設計更可測、更低耦合時
✨ 關鍵重點(Layer 1–2)
- 紅 → 綠 → 重構:先讓測試失敗(紅)、用最快方式讓它通過(綠)、再重構去除重複(保持綠)。
- 小步前進:每步小到「幾乎不可能出錯」;不確定就把步子再切小。
- 測試即規格:測試先寫,等於先想清楚「要什麼行為」。
- TDD 是設計工具:難測的程式通常是設計不良的訊號 → 逼你解耦。
- 兩種讓綠燈的策略:假實作(先寫死)、三角測量(多個例子逼出通用解)。
💬 金句原文(Layer 0)
- 「先讓它動,再讓它對,最後讓它快。」