測試驅動開發
輸出語言
預設使用繁體中文與用戶溝通,並以繁體中文編寫測試策略、測試說明和實作總結。用戶明確指定其他語言或既有文件語言另有約定時,遵從該約定;程式碼、命令、產品名和必須保持原文的引用不強行翻譯。
理念
核心原則:測試應透過公開介面驗證行為,而非實作細節。程式碼可以完全改變;測試不應受影響。
好的測試是整合風格的:它們透過公開 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。
在撰寫任何程式碼之前:
- 與用戶確認需要哪些介面變更
- 與用戶確認要測試哪些行為(確定優先順序)
- 當介面邊界不明確時,利用程式碼庫設計原則識別深度模組的機會。除非用戶明確要求,否則不要自動呼叫另一個技能。
- 列出要測試的行為(而非實作步驟)
- 獲得用戶對計劃的批准
提問:「公開介面應該長什麼樣子?哪些行為最重要,需要測試?」
你無法測試所有東西。 與用戶確認確切哪些行為最重要。將測試工作集中在關鍵路徑和複雜邏輯上,而不是每一個可能的邊界情況。
2. 曳光彈
撰寫一個測試,確認關於系統的一件事:
RED: 撰寫第一個行為的測試 → 測試失敗
GREEN: 撰寫最少程式碼以通過 → 測試通過
這是你的曳光彈——證明路徑能端到端運作。
3. 增量循環
對每個剩餘行為:
RED: 撰寫下一個測試 → 失敗
GREEN: 撰寫最少程式碼以通過 → 通過
規則:
- 一次一個測試
- 僅撰寫足以通過當前測試的程式碼
- 不要預測未來的測試
- 保持測試專注於可觀察的行為
4. 重構
當所有測試通過後,尋找重構候選:
- 提取重複的部分
- 深化模組(將複雜性移到簡單的介面後面)
- 在自然的情況下應用 SOLID 原則
- 考慮新程式碼揭示了關於既有程式碼的什麼資訊
- 每個重構步驟後執行測試
永遠不要在 RED 階段重構。 先讓它變成 GREEN。
每個循環的檢查清單
[ ] 測試描述行為,而非實作
[ ] 測試僅使用公開介面
[ ] 測試在內部重構後仍能存活
[ ] 程式碼對此測試而言是最少的
[ ] 沒有加入推測性的功能