🎯 什麼情境該想到我
當你的程式「很難寫測試」(依賴一堆、要 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表格測試與子測試 —— 測試寫出來之後的結構:表格驅動、子測試、失敗訊息