測試分兩層:綁契約的是資產,綁實作的是拋棄式
「改一條規格要動 50 個測試」和「refactor 一次整片紅」是同一個病:把拋棄式的那層當共享資產在維護。
| 層 | 綁什麼 | 數量 | 改動頻率 |
|---|---|---|---|
| 契約層 / integration | 模組對外 API、DB schema、跨線 interface、錯誤碼與狀態機 | 少而穩 | 只有契約真的變才紅——共享資產 |
| 內部 unit | 單一 class 內部分支、演算法 | 多 | 拋棄式:refactor 時連同實作刪掉重寫是正常的 |
- AC ↔ test 的綁定只發生在契約層,那層本來就應該很少。想像「每個 unit test 都要對應一條業務場景」才是雞肋。
- 內部 unit 層跟需求語言無關,它管的是設計回饋:難測 = 耦合過高。
unit / integration 的分界線
是否跨越一個我不能單方面改的邊界。
- unit:一個決策點的邏輯正確性(分支、邊界、計算、錯誤處理),全在記憶體、不碰 IO ——驗「算得對不對」
- integration:跨邊界的契約(真 DB、真 HTTP client、真 serialization)——驗「串起來對不對」
「測試變實作鏡像」反模式清單
- mock 掉所有相依,只斷言「有呼叫
repo.save()」→ 驗的是程式碼長相,不是行為 - 每個 private method 一個測試
- 深比對整包物件,欄位一加就全紅
- 測試名稱是方法名而非行為(
testCalculatevs過期訂單不計入折扣) - setter / getter、純轉發層的覆蓋率填充測試
原則
測試綁行為與契約,不綁路徑——斷言看對外可觀察結果,不看內部呼叫順序、私有方法、mock 呼叫次數。另外,一條規格改動打到 50 個 unit,常常只是那 50 個測試各自重建同一套 fixture → 用 test data builder 把 setup 收斂到一處。