Tests as a Control (測試作為對照標本)
概述
測試是一個對照標本:它固定住一個預期結果,使得行為的任何變化都變得可見。紀律是:不要修正測試來吻合程式碼;要理解它們為什麼不一致。
規則
-
行為變更時測試先行。 先寫出會失敗的測試,觀察它以正確的理由失敗,然後寫出最少量的程式碼讓它通過。如果你沒有觀察到它失敗,你就不知道它測試的是你預期的內容。這個循環本身是
superpowers:test-driven-development;本技能管轄的是測試必須包含什麼,以及一旦測試存在,失敗代表什麼意義。無論是否安裝了該技能,此規則都適用。 -
結構變更由未修改的測試套件驗證。 純粹的結構變更—重新命名、移動、拆分、提取,且無可觀察的行為變更—不需要新的測試。它的通過條件正好相反:整個現有的測試套件在未編輯任何測試的情況下通過。 這就是為什麼測試先行在此不適用—因為沒有行為需要指定,所以沒有測試能有意義地先失敗。如果一個結構變更迫使你編輯測試,那麼它就不是純粹的結構變更。停止並使用下一條規則重新分類。
-
跨越合約邊界的重新命名不是重構。 問題不在於「程式碼是否改變了行為?」而在於「這個名稱對於我不控制的東西來說是可觀察的嗎?」
重新命名的項目 分類 通過條件 內部符號、私有輔助函式、本地檔案 結構變更 現有測試套件未經編輯即通過 匯出的函式、API/GraphQL 欄位、路由、CLI 標誌、資料庫欄位、事件名稱、設定鍵 行為變更—它會破壞消費者 測試先行 + 新增並棄用 公開重新命名對相同的資料執行相同的操作,但仍然是一個破壞性變更,因為名稱本身就是合約(Hyrum's Law — 請參閱
performance-and-scale)。將其視為行為變更:透過測試先行循環新增新名稱,在棄用視窗期間保留舊名稱及其通過的測試,只有在消費者遷移後才移除舊名稱。第三種情況是測試中以字串形式引用名稱—快照、DI 容器鍵、反射、fixture 路徑。這些在內部重新命名時會中斷。這不是規則失效;而是測試與實作細節耦合的訊號。修復耦合,不要重新基線化。
-
重構不得編輯測試;需求變更才可以。 不同的行為:
- 重構導致測試失敗 → 測試發現了非預期的行為變更。停下來並理解它。 為了讓重構通過而編輯測試,會毀掉你的儀器。
- 需求真正改變了 → 刻意地更新測試,作為其自身可審查的變更,並如實記錄。
- 禁止的行為是默默地重新基線化。
-
對稱覆蓋 — 每個規則都需雙向測試。 證明授權的行為者能夠 以及 未授權的行為者不能。僅測試快樂路徑只測試了半條規則。
-
權限/角色規則是個矩陣: 每個有意義的 (角色 × 動作 × 資源) 單元格,都需測試允許和拒絕。一個新角色在其拒絕案例存在之前是不完整的。一個原本應該拒絕的測試突然通過,是一個安全性回歸—必須明確審查並接受,絕不重新基線化。
-
進入點絕不能默默地新增/移除。 路由、命令、端點都需要存在性與冒煙測試覆蓋,這樣新增/移除動作會強制測試變更。
-
按規模測試: 小型(純邏輯,無 I/O,確定性)、中型(真實資料庫/行程,隔離環境)、大型(完整堆疊)。多數小型,較少中型,最少大型。留意缺失的中間地帶—伺服器/應用程式邏輯既未被單元測試也未被整合測試覆蓋,因為每一層都假設另一層會測試它。
-
不穩定的測試一經發現立即隔離。 在沒有程式碼變更的情況下時而通過、時而失敗的測試會破壞對整個測試套件的信任。立即隔離(移出阻斷閘門,提交修復工單)—絕不留下間歇性失敗的測試,也絕不默默地刪除。小型測試必須是確定性的:不應使用真實時鐘、隨機性或網路。
-
測試程式碼應是 DAMP,而非 DRY。 Descriptive And Meaningful Phrases(描述性且有意義的短語) 優於去重複化。測試必須能在隔離狀態下被直觀理解。將 DRY 應用於測試是一種違規,而非美德(請參閱
change-hygiene-and-code-craft)。
常見的合理化藉口
| 藉口 | 現實 |
|---|---|
| 「我先寫程式碼,然後再補測試」 | 那麼測試就是為了配合程式碼,而非配合需求。你從未觀察到它失敗,所以你不知道它是否真的在測試任何東西。 |
| 「只是重新命名,不需要測試」 | 正確—如果名稱是內部的話。如果名稱是匯出/公開的,那麼名稱本身就是合約,重新命名就是行為變更。 |
| 「我重新命名了它,然後不得不修正三個測試,這很正常」 | 這不正常。要麼你跨越了合約邊界,要麼那些測試與實作細節耦合。重新分類。 |
| 「測試現在錯了,我只要更新它就行了」 | 只有當需求改變時才能這麼做。如果你正在重構,測試發現了真正的變更—請調查。 |
| 「我測試了快樂路徑,這樣就夠了」 | 在拒絕案例也測試過之前,一條規則不算被測試過。 |
| 「這個拒絕測試現在通過了,太好了,快發布」 | 一個被禁止的動作成功了是安全性回歸,不是進展。 |
| 「測試不穩定,只要重新執行 CI 就好」 | 不穩定性會掩蓋真正的錯誤,並讓每個人都學會忽略失敗。隔離並修復。 |
| 「讓我用一個共享輔助函式來 DRY 這些測試」 | 測試是 DAMP 的。有助於原地清晰度的重複應該保留。 |
危險信號 — 立即停止
- 在行為變更的失敗測試存在之前,就先撰寫生產程式碼
- 一個結構變更的 diff 同時觸及了測試檔案
- 原地重新命名公開/匯出的名稱,而非使用新增並棄用策略
- 在重構過程中編輯測試以讓它變綠
- 新的權限沒有拒絕案例的測試
- 在未理解快照/斷言為何改變的情況下重新基線化
- 包含真實
sleep、Date.now()、隨機資料或網路呼叫的測試 - 為了消除重複而犧牲可讀性的測試程式碼重構
完整理由說明:憲法第四條,收錄於 engineering-constitution/references/engineering-constitution.md。關於 GraphQL/路由合約的具體細節,請參閱 graphql-contract-testing。