測試驅動開發
哲學
核心原則:測試應透過公開介面驗證行為,而非實作細節。程式碼可以完全改變;測試則不應隨之變動。
好的測試是整合風格的:它們透過公開 API 實際執行程式碼路徑。它們描述的是系統「做什麼」,而不是「怎麼做」。好的測試讀起來像一份規格書——「使用者可以使用有效的購物車結帳」精準告訴你存在哪些功能。這類測試能夠在重構後存活,因為它們不關心內部結構。
糟糕的測試則與實作緊密耦合。它們模擬內部協作者、測試私有方法,或透過外部手段 (例如直接查詢資料庫,而不透過介面) 進行驗證。警示訊號:當你重構時,行為並未改變,但測試卻失敗了。如果你重新命名一個內部函數而測試失敗,代表那些測試驗證的是實作,而非行為。
請參閱 tests.md 獲取範例,以及 mocking.md 取得模擬指南。
反模式:水平切片
請勿先寫完所有測試,再寫所有實作。 這就是「水平切片」——把 RED 階段當作「寫全部測試」,GREEN 階段當作「寫全部程式碼」。
這會產生糟糕的測試:
- 批次撰寫的測試驗證的是 想像中的 行為,而非 實際的 行為
- 最終你測試的是事物的 形狀 (資料結構、函數簽名),而非使用者面對的行為
- 測試對真正的變更不敏感——行為壞掉時測試通過,行為正常時測試反而失敗
- 你超出了自己的掌控範圍,在理解實作之前就承諾了測試結構
正確做法:透過五金測試進行垂直切片。一個測試 → 一個實作 → 重複。每個測試都回應你在前一個循環中學到的東西。因為你剛剛才寫完程式,你清楚知道哪些行為重要以及如何驗證它們。
錯誤 (水平):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
正確 (垂直):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
...
工作流程
1. 規劃
瀏覽程式碼庫時,請使用專案的領域詞彙表,使測試名稱和介面詞彙與專案語言一致,並尊重你所接觸區域的 ADR。
在撰寫任何程式碼之前:
- 與使用者確認需要哪些介面變更
- 與使用者確認要測試哪些行為 (排定優先順序)
- 找出 深度模組 的機會 (小介面,深實作)
- 為 可測試性 設計介面
- 列出要測試的行為 (而非實作步驟)
- 獲得使用者核准該計劃
詢問:「公開介面應該長什麼樣子?哪些行為最重要,需要測試?」
你無法測試所有東西。 與使用者確認哪些行為最為重要。將測試心力集中在關鍵路徑與複雜邏輯上,而非每個可能的邊界狀況。
2. 五金測試
撰寫一個測試,確認系統的一件事:
RED: 為第一個行為撰寫測試 → 測試失敗
GREEN: 撰寫最簡實作來通過 → 測試通過
這就是你的五金測試——證明路徑從端到端是可行的。
3. 增量循環
針對每個剩餘的行為:
RED: 撰寫下一個測試 → 失敗
GREEN: 最簡程式碼來通過 → 通過
規則:
- 一次只做一個測試
- 只撰寫足以通過當前測試的程式碼
- 不要預先構想未來的測試
- 讓測試專注於可觀察的行為
4. 重構
在所有測試通過後,尋找 重構候選:
- 提取重複的程式碼
- 深化模組 (將複雜度隱藏在簡單的介面之後)
- 在自然之處應用 SOLID 原則
- 思考新程式碼揭露了哪些關於既有程式碼的資訊
- 每個重構步驟後執行測試
永遠不要在 RED 狀態下進行重構。 先達到 GREEN。
每個週期的檢查清單
[ ] 測試描述的是行為,而非實作
[ ] 測試只使用公開介面
[ ] 測試在內部重構後仍能存活
[ ] 程式碼是為了這個測試的最簡實現
[ ] 沒有加入推測性功能