測試驅動開發
哲學
核心原則:測試應透過公開介面驗證行為,而非實作細節。程式碼可以完全改變;測試不該受到影響。
好的測試屬於整合風格:它們透過公開 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. 規劃
在探索程式碼庫時,請閱讀 CONTEXT.md(如果存在),以便測試名稱和介面詞彙與專案的領域語言一致,並尊重你所觸及區域中的 ADR。
在撰寫任何程式碼之前:
- 與使用者確認需要哪些介面變更
- 與使用者確認要測試哪些行為(優先排序)
- 找出深度模組的機會(小介面、深層實作)——執行
/codebase-design技能以取得詞彙和可測試性檢查 - 列出要測試的行為(而非實作步驟)
- 取得使用者對計畫的批准
問:「公開介面應該長什麼樣子?哪些行為最重要要測試?」
你無法測試所有東西。 與使用者確認哪些行為最重要。將測試工作集中在關鍵路徑和複雜邏輯上,而非每個可能的邊緣情況。
2. 追蹤子彈
撰寫一個測試,確認系統中的一件事:
紅: 撰寫第一個行為的測試 → 測試失敗
綠: 撰寫最簡程式碼使其通過 → 測試通過
這就是你的追蹤子彈——證明路徑端到端可行。
3. 增量循環
針對每個剩餘行為:
紅: 撰寫下一個測試 → 失敗
綠: 最簡程式碼使其通過 → 通過
規則:
- 一次只寫一個測試
- 只寫足以通過目前測試的程式碼
- 不要預先設想未來的測試
- 保持測試專注於可觀察的行為
4. 重構
在所有測試通過後,尋找重構候選:
- 消除重複
- 加深模組(將複雜性移到簡單介面背後)
- 在自然之處應用 SOLID 原則
- 思考新程式碼對既有程式碼揭示了什麼
- 在每個重構步驟後執行測試
絕不在紅燈時重構。 先達到綠燈。
每次循環的檢查清單
[ ] 測試描述的是行為,而非實作
[ ] 測試只使用公開介面
[ ] 測試在內部重構後仍能存活
[ ] 程式碼是為此測試所需的最簡版本
[ ] 未加入推測性的功能