測試套件醫生 (test-suite-doctor)
將一個過於龐大的測試套件(例如超過 2000 個測試,很多是 AI 生成的垃圾測試) 縮減成一個小型、高品質的套件,同時保留可設定的實際覆蓋率 — 以數據衡量,而非憑猜測。
硬性規則
- 先有數據,再下判斷。 在
collect和minimize產生計劃之前,絕對不要提議刪除任何測試。光靠閱讀測試程式碼就認定它「沒用」並不是證據 — 一個看似微不足道的測試,可能是某個分支的唯一覆蓋者。 - 計劃是提案,不是定論。 覆蓋率無法看見 API 合約、有文件記載的回歸案例,或屬性不變量。每個被建議刪除的候選項目都必須先經過審查(步驟 3),才能被刪除。
- 在分支上操作。 在刪除任何東西之前,請先確認工作目錄是乾淨的,並建立一個分支(例如
test-doctor/minimize)。絕對不要在骯髒的工作樹或預設分支上刪除測試。 - 驗證通過或復原。 工作尚未完成,直到
verify返回 0。如果驗證失敗,請針對回報的缺口重新產生測試並重新驗證 — 或者復原被刪除的測試。
前置需求檢查(請先執行此步驟)
- Node.js ≥ 22 以及一個使用本地安裝之 Vitest 或 Jest 的 package.json(
node <skill-root>/dist/cli.mjs collect --help)。 - 必須安裝覆蓋率提供者(Vitest:
@vitest/coverage-v8;Jest 已內建一個)。如果缺少,請主動提議將其安裝為開發依賴。 - 突變驗證額外需要在目標儲存庫中配置
@stryker-mutator/core— 這是選擇性加入的功能,請勿在未經要求的情況下安裝它。 - 測試套件在開始前應該是綠燈狀態。如果基線執行報告顯示有失敗,請告知使用者並取得他們對於如何處理這些失敗的同意,然後再進行最小化(失敗的測試會產生不可靠的覆蓋率)。
以下所有命令都從目標儲存庫的根目錄執行;$SKILL 是此技能的目錄。產出的檔案會放在 .test-doctor/ 中(建議將此目錄加入 gitignore)。
工作流程
1. 衡量(MEASURE)(腳本 — 永遠第一步)
node "$SKILL/dist/cli.mjs" collect --cwd .
偵測測試執行器,先完整執行一次測試套件作為覆蓋率基線,然後隔離每個測試檔案單獨重新執行,記錄其覆蓋的行數/分支與執行時間。寫入 .test-doctor/report.json。
- 預設的細粒度是每個測試檔案(每個測試分別隔離執行是精確的,但會使每個測試都獨立執行 — 僅在小型或已縮減的套件,或使用了
--filter限縮範圍時,才提議使用--granularity test)。 - 對於一個有 2000 個測試的套件,這會花費一些時間;請在背景執行並回報進度。請認真看待報告中的
collectionErrors— 這些單元預設會阻礙最小化流程。如果使用者明確選擇了--keep-unmeasured,則每個受影響的單元都將是強制保留的,且計劃將被標記為不可信任。 --filter會建立一個特意限縮範圍的基線:收集、最小化和驗證僅涵蓋匹配的測試檔案。請在每個建議中說明這個範圍;這並非整個儲存庫的保證。
2. 最小化(MINIMIZE)(腳本)
node "$SKILL/dist/cli.mjs" minimize --coverage-floor 0.97 --target-count 200
貪婪加權選擇:反覆保留在每毫秒執行時間內,能提供最多新行/分支覆蓋率的單元,直到達到覆蓋率下限。寫入 .test-doctor/plan.json 以及一個人類可讀的 .test-doctor/plan.md,其中包含每個保留與捨棄單元的理由。
- 如果使用者未指定,請詢問他們想要的閾值;預設值是 0.97 的行覆蓋率下限、無分支覆蓋率下限、無目標數量。
--help會列出所有可調整的參數。 - 在開始前,強制保留已知的合約/回歸測試:
--keep 'contract' --keep 'regression'(對單元 ID 使用正規表示式)。 - 請留意計劃中的
warnings陣列 — 例如無法達到覆蓋率下限,或需要比目標數量更多的單元。請如實將其轉達給使用者。
3. 審查(REVIEW)(判斷 — 你的)
閱讀 references/slop-patterns.md,然後審查 plan.md 中的捨棄清單:
- 拯救任何用於驗證覆蓋率無法看見之行為的測試:公開 API 合約、參考了問題/錯誤 ID 的回歸測試、被消費者依賴的錯誤訊息措辭、屬性/不變量檢查。請使用額外的
--keep參數重新執行最小化,而不是手動編輯計劃。 - 確認其餘的測試都是安全的刪除;根據垃圾模式對它們進行分類,讓使用者了解為什麼每個測試會被拋棄(模式目錄中有分類清單)。
接著,根據 references/quality-rules.md 審查保留清單:對於有異味的保留測試(mock-the-mock、同義反覆、空殼快照),請在原地重寫它們 — 它們贏得了保留的資格,但應該要乾淨地贏得資格。
只有在完成此審查之後,才能在分支上刪除已決定捨棄的測試檔案/案例。當單元的所有測試都被捨棄時,刪除整個檔案;當只有檔案中的部分測試被捨棄時,編輯檔案(測試層級的細粒度)。
4. 重製(REGENERATE)(判斷 — 你的)
先執行一次 verify(預期它可能會失敗)來取得覆蓋率缺口報告,然後根據 references/quality-rules.md,為 verify 所列出的受影響最嚴重的原始碼檔案撰寫新的測試:AAA 結構、行為驅動的命名、每個測試單一行為、只在外部邊界使用 mock。寧可寫一個能涵蓋真實使用者可見行為的強健測試,也不要三個淺層測試。
5. 驗證(VERIFY)(腳本 — 作為完成與否的關卡)
node "$SKILL/dist/cli.mjs" verify --coverage-floor 0.97
# 可選擇,僅針對關鍵模組(較慢):
node "$SKILL/dist/cli.mjs" verify --mutation --mutate "src/billing/**/*.ts" --mutation-floor 80
重新執行測試套件,要求結果為綠燈,且要求行覆蓋率保留率需達到與基線相比的下限。回傳 0 = 完成;回傳 1 = 修復並重新執行(重製更多測試,或復原刪除的測試);回傳 2 = 環境問題,停止並回報。突變測試因為很慢,所以是針對模組選擇性加入的 — 針對使用者認為關鍵的模組來提供此選項。
第二版會建立涵蓋原始碼、執行器設定、鎖定檔、執行器和覆蓋率提供者的指紋。如果 verify 回報有來源漂移,請重新收集一個新的基線。只有在取得使用者明確同意時,才能使用 --allow-provenance-drift,並將最終的結果報告標記為不可信任。舊版的第一版基線同樣需要明確的 --allow-legacy-baseline 逃脫艙口才能使用。
回報結果
為使用者總結:單元數量前 → 後、行/分支覆蓋率保留率、執行時間前 → 後、發現的垃圾模式(附數量)、從捨棄清單中拯救的測試及其原因、重製的測試,以及最終的 verify 結果。請包含 plan.md 的路徑,以提供完整的稽核軌跡。
失敗模式
- 無法偵測/無法判別執行器 → CLI 退出碼 2 並提供指引;請傳遞
--runner或--cwd(monorepo:在每個套件中分別執行)。 - 收集不完整 → 修復隔離的失敗。不要只是為了讓計劃通過而將它們過濾掉;篩選條件會改變已宣告的基線範圍。
--keep-unmeasured是保守的逃脫艙口。 - 重製後 verify 持續失敗 → 大約嘗試 3 次後停止循環;逐檔案呈現殘留的缺口,讓使用者決定是要降低覆蓋率下限還是保留更多測試。
- 開始前測試套件是紅燈狀態 → 不要默默「修復」不相關的失敗;先回報它們,並先就處理範圍達成共識。