Day 29|這支測試怎麼有時通過、有時失敗?
前言
昨天沿著紅燈留下的證據找原因,今天遇到的測試更麻煩:同樣條件下,這次紅、下次卻綠了。我們先決定跑幾次,再記下失敗次數,把「有時候會壞」量出來,交給後續流程處理。
開始之前
一支測試時紅時綠,最自然的反應是重跑一次。綠了,收工。
這個反應會製造兩種傷害。第一,訊號被關掉。 你用重跑蓋掉了一個真實存在的時序問題,它下週會再來,而且下次可能發生在真正重要的那一次發布前。第二,人開始不信 CI。 一旦「紅了先重跑」變成習慣,真正的產品回歸也會被重跑掉。
但 flaky 也不能拿 Day 28 那套處理,因為它不穩定,你沒辦法用一次觀察歸類。「它有時候會紅」不是可以行動的描述。
所以這一步的工作是把「有時候」變成一個數字:同一份程式碼、同一個環境,連續跑 N 次,紅了幾次。有了這個數字,才談得上隔離、修法、或解除隔離。
這次先別用「失敗就重試」讓報告變綠,也別只把固定等待拉長就宣布修好了。它們可能讓失敗比較少出現,但還沒回答測試到底應該等到什麼條件才往下走。
還有一個前提要說明:本專案的設定是 retries: 0,重試會把 flaky 蓋掉,而這裡就是要看見它。
動手試試
配套專案有一支示範用的競態測試,等待時間由 FLAKY_WAIT_MS 控制。一次選一個等待值,各跑 20 次,例如先量 1350 毫秒:
FLAKY_WAIT_MS=1350 npm run test:flaky # --repeat-each=20, workers=4跑完之後
換不同等待值量測,結果如下。每一輪都是 20 次,1350 和 1400 毫秒另外各量了一輪;「—」表示這份紀錄沒有第二輪數字:
| 等待時間(毫秒) | 第一輪失敗次數/20 | 第二輪失敗次數/20 | 依本次樣本判斷 |
|---|---|---|---|
| 700 | 20/20(100%) | — | 本輪全紅,先按持續失敗處理 |
| 1000 | 20/20(100%) | — | 本輪全紅,先按持續失敗處理 |
| 1200 | 18/20(90%) | — | flaky,失敗率很高 |
| 1300 | 18/20(90%) | — | flaky,失敗率很高 |
| 1350(預設值) | 7/20(35%) | 12/20(60%) | flaky,兩輪差 25 個百分點 |
| 1400 | 2/20(10%) | 5/20(25%) | flaky,兩輪差 15 個百分點 |
兩個結論,一個比一個重要。
第一,同一種等待問題,可能呈現不同症狀。 700ms 這輪二十次全紅,1350ms 則有紅有綠。前者先走昨天的失敗分析,後者記下失敗率再查原因。1200ms 和 1300ms 雖然紅了十八次,仍然有兩次綠,依今天的判準也算 flaky。
第二,同一個等待值,兩輪量到的比例也不一樣。 1350ms 是 35% 和 60%,差 25 個百分點;1400ms 是 10% 和 25%,差 15 個百分點。所以回報時要把分母一起寫出來,「二十次失敗七次」才知道你到底量了多少。
同一份 repo 還有一個附帶發現,寫在 tests/README.md 裡:broken/ 那批示範用的壞測試,第一版有一支綁死顯示文字,它自己就會飄(第一次紅、第二次綠)。示範用的壞測試自己也是 flaky,可見這東西有多容易不小心種進去。後來改成綁死一個不存在的 CSS class,才變成確定性失敗。
背後怎麼做
三種結果,只有一種叫 flaky
同一份程式碼、同一個環境,重複執行 N 次:
| 結果 | 判定 | 下一步 |
|---|---|---|
| 有紅有綠 | flaky | 量化重現率,往下走 |
| N 次全紅 | 本輪沒有觀察到時紅時綠,先按持續失敗處理 | 交 Day 28 的分流 |
| N 次全綠 | not-reproduced |
記錄後結案 |
這張表是在決定本輪往哪裡送,不是在替測試保證一輩子。全紅不代表它永遠不會綠,全綠也不代表它永遠不會再失敗。
先決定 N,再開始跑,不要一直重跑到出現自己想要的結果才停。N 到了就記錄這輪的觀察;之後若出現新證據,再另開一輪。重跑次數與驗收門檻都讀設定檔,不臨時改到結果好看為止。
還有一條硬性要求:沒有重現率的 flaky 回報不算數。 必須記「幾次紅、總共幾次」,而且保留每一次的失敗訊息。理由很實際:不同次紅的原因不同,通常代表有好幾個問題疊在一起,那要拆開報,不是報成一支 flaky。
根因五類,以及交下游時的那一欄
| 疑似根因 | 訊號 |
|---|---|
wait-condition |
元素存在但還不可用 |
data-pollution |
測試間資料殘留、互相污染 |
parallel-race |
平行執行搶同一筆資料或同一個帳號 |
external-dependency |
依賴外部服務、時間、時區、亂數 |
render-timing |
動畫、非同步渲染、重繪造成的時序 |
交下游的時候一定要帶 classification 那一欄,而且要用 Day 28 那張表的詞彙。原因很機械:修法表是用那組詞彙當 key 的,只給疑似根因,下游查無此列,修不了。
輸出長這樣:
nodeid: "<file> > <test name>"
runs: 20
failures: 7
flake_rate: 7/20
suspected_root_cause: wait-condition
classification: wait-timing # 下游認得的詞彙,缺這欄它修不了
confidence: medium
next_step: "改成 web-first assertion"今天的 flaky-detect 負責確認是否有紅有綠、記下比例和疑似原因,再交給對應流程修。修完後,由重跑驗證檢查是否達到設定的連續通過次數;通過的是這次驗收,不能保證以後永遠不會再紅。
至於隔離,是先把這支不穩定測試從會擋住發布的檢查中分開,持續追蹤,修好且驗收通過後再放回去。這由 flaky-manager 管理,今天這輪只有量測,沒有實際執行隔離或解除。
今天學到什麼
今天把「它有時候會紅」換成了具體數字:跑二十次,失敗幾次。這輪全紅的先走持續失敗分析,有紅有綠的記下比例和原因,全綠的則記錄本次沒有重現,不硬湊答案。
同一個 1350ms 等待值,兩輪分別失敗七次和十二次;1400ms 則是兩次和五次。一次量到的比例不會替你保證下一次,所以要保留樣本數與失敗訊息。今天先完成量測,修復、隔離與解除仍要交給對應流程處理。
明天是最後一天,要把前面的工作串成一輪值班。哪些可以自己接著做、哪些需要授權、遇到什麼情況要停,會一起交代清楚。
參考資料
- Qingzhou Luo、Farah Hariri、Lamyaa Eloussi、Darko Marinov(2014)《An Empirical Analysis of Flaky Tests》,FSE 2014 - 大規模分析 flaky 測試的成因分布,本篇五類根因的來源
- John Micco(2016)《Flaky Tests at Google and How We Mitigate Them》,Google Testing Blog - 工業規模下的 flaky 治理:偵測、隔離、解除,以及為什麼不能靠重跑
- Martin Fowler(2011)《Eradicating Non-Determinism in Tests》 - 不確定的測試會讓整套測試失去意義,以及隔離為什麼是止血不是解法
- Edwin B. Wilson(1927)《Probable Inference, the Law of Succession, and Statistical Inference》,Journal of the American Statistical Association 22(158) - 比例的信賴區間:小樣本下的重現率誤差有多大,正是「N 夠不夠大」的計算依據
- 本專案
skills/maintain/flaky-detect/SKILL.md(判準與五類根因)、tests/flaky/render-race.spec.ts(掃描實測)、tests/README.md的 flaky 那一節、references/green-cheating.md- 判準、五類根因與掃描實測的真檔