Flaky test 累積到某個密度後,團隊的反應從「調查失敗原因」轉變為「再跑一次看看」。再跑一次還是紅,就標記 skip。Skip 的測試愈多,測試套件的覆蓋面愈窄——但 CI 的綠燈不會告訴你這件事。問題從「測試品質」升級成「測試信任」:開發者開始懷疑紅燈是不是真的代表問題,真正的 bug 在這個懷疑裡被忽略。

Flaky test 根因分類教的是單條 flaky test 的診斷與修復——計時依賴、環境差異、資源競爭、非確定性輸出,每類有對應的處理策略。本章往上一層:當 flaky test 的數量超過個案處理的能力時,團隊需要一套治理機制。

Quarantine 政策

Quarantine 是把已知 flaky 的測試從主要執行路徑移開,讓它的紅燈不污染 CI 的通過訊號,同時保留修復的壓力。

隔離的觸發條件

觸發隔離的訊號是「同一條測試在程式碼沒有改變的情況下,最近 N 次執行中失敗 M 次」。N 和 M 的校準邏輯:N 取一個觀察窗裡同一條測試在 CI 上的執行次數(每日一次、兩週 = 10-14);M 的語意是「超過這個次數就不太可能全是環境噪音」。基準率要用自己團隊的數字:取近兩週所有「程式碼沒有變更卻重跑」的 CI 記錄,數其中因環境因素(連線逾時、資源不足、外部服務抖動)失敗的比例——這就是基準率,多數團隊落在 2-8%。以下用 5%(每 20 次執行中有 1 次)示範推導,N=10 時純噪音連中 3 次的機率低於 1.2%,足以區分。CI 頻率更高(每 PR 觸發)時 N 可放大:同樣以 5% 基準率與 1.2% 門檻反解二項分布,N=20 時 M=5、N=30 時 M=6(M 隨 N 成長得比線性慢——這是分布的性質,按比例外推會過度保守)。換基準率就重解一次,公式是 P(X≥M) < 門檻、X~B(N, 基準率)。

手動標記也是一條路:開發者在本機或 code review 中發現 flaky 行為後,主動提報隔離。兩條路互補——自動偵測抓到頻繁犯案的,手動提報抓到偶爾犯案但已被人觀察到的。

隔離後的責任分配

隔離不是遺忘。每條被隔離的測試需要一個指定的負責人和一個回收期限。

負責人的指定有兩種策略:最後修改該測試的人(git blame),或擁有該功能模組的團隊成員。兩者各有適用情境——如果 flaky 的根因在被測程式碼(例如 fire-and-forget 編排的競態),功能模組擁有者更合適;如果根因在測試本身(例如 assertion 設計依賴執行順序),最後修改者更接近脈絡。

回收期限是一個行動閾值:到期時測試還沒修好,升級為技術債討論——是修復成本太高、還是這條測試的覆蓋價值不值得投資。兩者的處置不同:成本太高代表需要改被測程式碼的設計,而非只改測試;價值不夠代表刪除測試比維護它的 ROI 更好。

Re-admit 條件

修復後的測試回到主要執行路徑之前,需要通過一個確認期:在隔離環境連續成功 N 次。N 的校準邏輯與觸發條件對稱——觸發用的 M 次失敗代表「這個頻率不是噪音」,確認期的 N 至少是 M 的 2-3 倍,代表「修復後的穩定度顯著高於觸發時的不穩定度」。這個倍數是經驗值、沒有像觸發門檻那樣的機率推導——它要平衡的是確認強度與回收速度,倍數放大會讓修好的測試困在隔離區更久。例如觸發門檻是 10 次中失敗 3 次,確認期至少連續成功 6-9 次。確認期的目的是排除「修復恰好讓它在最近幾次通過、但根因未消除」的情境。

Re-admit 後仍有復發的可能。同一條測試若在一段期間內被隔離超過兩次,升級處置:這條測試的穩定性問題可能來自被測程式碼的結構(而非測試技巧),修復方向從測試層轉向設計層。

Retry 預算

CI 中的 retry(失敗後自動重跑)是處理環境噪音的常見手段,但不設限的 retry 會掩蓋真正的 flaky test。Retry 預算把重跑從「無限制的安慰機制」轉變為「有限額的環境噪音容忍」。

預算的結構

  • 每條測試的 retry 上限:通常 1-2 次。超過上限仍然失敗,記為真失敗。上限的意思是:環境噪音值得容忍的次數——超過這個次數,噪音本身就是需要處理的問題。
  • 整個套件的 retry 總量上限:一次 CI 執行中,所有測試加總的 retry 次數上限。單條測試各自在自己的上限內,但整體 retry 次數過高代表套件的穩定性已經是系統性問題、個案處理不夠。校準邏輯:上限 = 測試總數 × 可容忍的 retry 比例。若團隊認定「一次 CI 執行中有 5% 的測試需要 retry」是可接受的噪音水準,200 條測試的上限就是 200 × 5% = 10 次——超過代表套件整體的環境噪音已超出個案處理的能力。這裡的 5% 是「多少比例的測試會 retry」,不是「每條測試平均 retry 幾次」,兩者混用會讓上限算出 20 倍的差距。
  • Retry 通過的語意:retry 後通過算不算通過?兩派做法:算通過但標記(CI 輸出 passed on retry,不阻擋合併);或算 warning(CI 通過但產生一條警示,累計 warning 數量)。選擇取決於團隊目前的 flaky 密度——密度高的初期,retry 通過算通過,先讓 CI 能用;密度降到可控範圍後升級為 warning,推動修復。切換點可以直接借用下節的行動閾值:skip 比率與 retry 總量都回到閾值以下時,就是升級的時機,不必另立一套判準。

Retry 計數作為指標

Retry 的次數本身是一個趨勢指標:

  • 週 retry 次數穩定下降 → 修復策略生效
  • 週 retry 次數穩定或上升 → 新增的測試在製造 flaky,或既有根因未消除
  • 特定測試檔案的 retry 佔比過高 → 定位到需要優先處理的模組

這個指標的價值在於趨勢而非絕對數字——每個團隊的「可接受 retry 率」不同,關鍵是方向。

信任修復

Skip 率和 retry 率的可視化是治理機制的回饋迴路。數字放在團隊看得到的地方(CI dashboard、週報、sprint review),把「測試信任度」從感覺轉變為量化。

可視化的指標

  • Skip 比率:被 skip 的測試數 / 總測試數。這個數字的意義:套件覆蓋面的實際收縮幅度。10% 的測試被 skip,代表 CI 綠燈只覆蓋了 90% 的預期範圍。
  • Quarantine 佔比:被隔離的測試數 / 總測試數。跟 skip 比率的差異:skip 是主動放棄、quarantine 是暫時移開但有回收計畫。quarantine 佔比高但有回收期限和負責人,跟 skip 比率高但無人管,是兩種本質不同的狀態。
  • Retry 率:retry 次數 / 總測試執行次數。反映環境噪音的程度。
  • Quarantine 平均停留時間:被隔離的測試平均在隔離區待多久。這個數字持續增長代表回收機制失效——測試進得去、出不來。

行動閾值

數字化之後需要配套的行動閾值,否則指標只是好看的圖表:

  • Skip 比率 > X% → 凍結新功能開發,優先處理 skip 積壓。X 的校準邏輯:skip 的每一條測試都是「CI 綠燈不再覆蓋的範圍」。把 X 設在團隊能接受的覆蓋縮水幅度——若團隊認為「綠燈覆蓋少 5% 就不可信」,X = 5%(200 條測試中 10 條被放棄)。小團隊初期可以寬鬆(10%),套件成熟後收緊。
  • Quarantine 平均停留時間 > Y 天 → 強制 triage:修復、刪除、或承認這條測試的覆蓋目標需要重新設計。Y 的校準邏輯:隔離是暫時措施,停留時間反映的是「修復排入工作流的速度」。Y 設在團隊的迭代週期——一個 sprint 週期(14 天)是合理起點,代表「如果一個 sprint 都排不進修復,要升級決策」。
  • 連續 N 天 retry 率上升 → 觸發一次 flaky test 的集中清理(例如用半天做 flaky test fix-a-thon)。

行動閾值的功能是把「品質劣化」從漸進式、無感的過程轉變為離散的、需要回應的事件。

與個案修復的關係

團隊治理和個案修復是兩個層次的工作:

  • 個案修復的入口是 flaky test 根因分類——單條 flaky 的診斷與修復策略
  • 團隊治理提供的是結構:哪些該先修、修不好的怎麼處理、整體趨勢往哪走

兩者的互動點:quarantine 區的測試依根因分類排定修復優先序(計時依賴和資源競爭通常比環境差異更容易修);retry 通過的測試用根因分類判斷是否值得追查(非確定性輸出型通常改 assertion 就好、計時依賴型需要改被測程式碼)。

工具化路徑

Quarantine 與 retry 的實作依測試框架而異,選型時看兩件事:框架是否原生支援 retry 計數與 tag 排除、CI 平台是否提供 retry 報告。

  • 隔離的實作:test tag 或 annotation(@quarantine@Tags(['quarantine']))配測試 runner 的排除參數(--exclude-tags quarantine)。目錄分離是另一條路,但 tag 的好處是測試仍在原目錄、git blame 和搜尋不受影響。
  • 自動偵測 flaky:CI 結束後解析 JUnit XML(或 runner 原生的 JSON 報告),統計同一 test name 最近 N 次執行的 failure 頻率,超閾值自動加 quarantine tag 或開 issue。低成本起步:pipeline 結尾加一支腳本查詢 CI artifact 內的歷史報告;成熟方案如 Bazel 的 --runs_per_test 或 GitHub Actions 的 flaky test detection 外掛。
  • Retry 的實作:多數 runner 有原生支援或外掛——Jest 的 --retries、pytest 的 pytest-rerunfailures、Dart test 的 --retry。重點是 retry 通過時的輸出格式——CI 報告裡能不能區分「首次通過」和「retry 後通過」,決定 retry 計數指標的可收集性。
  • 指標的收集:CI 平台的 test report 外掛通常能輸出 JUnit XML,從中提取 retry 計數與 skip 計數。低成本起點:CI pipeline 結束時用腳本從報告檔解析三個數字(skip 數、retry 數、quarantine 數)寫進一行 CSV 或推送到團隊頻道——趨勢用試算表畫。

下一步路由