對抗 flaky:穩定 locator、狀態清理、retry 與 quarantine
#flaky #測試穩定性 #pytest #QA 實戰
CI 亮紅燈,你重跑,綠了。沒有改任何程式碼,就這樣過了。flaky 測試比完全沒有測試更危險,因為它訓練你忽略紅燈。一旦團隊學會「重跑就好」,真正的 bug 出現時,沒有人會認真看。 flaky 的成因 flaky 通常來自這四個地方: | 成因 | 描述 | | | | | 時序問題 | 沒等 UI 就繼續操作,time.sleep 猜秒數,CI 機器慢就掛 | | 狀態殘留 | 上一條測試建的資料還留在 DB,下一條撞名衝突或撿到舊值 | | locator 脆弱 | 用 XPath 抓位置、用 class 抓動態 style,元件稍微改版就斷 | | 環境差異 | 裝置效能不一、網路抖動、CI 跑在共享資源機器 | 前三項你可以治本;第四項才是真正的「隨機性」,也是 retry 唯一合理的用武之地。 治本:穩定 locator 與狀態清理 穩定 locator 的原則(locator 那篇講過):優先用 accessibility id 或 resource id,不用 XPath 抓層級、不用 class 抓視覺。 狀態清理 要靠 fixture 在每條測試前後各做一次。不要假設上一條測試沒有留下痕跡。 autouse=True 讓這個 fixture 在整個 scope 內自動套用,不必在每條測試手動掛。teardown 放在 yield 之後,即使測試中途失敗也保證執行。 顯式等待搭配乾淨狀態,把 flaky 機率壓到最低,兩件事合在一起才完整。上面用 API 清資料是一種做法;我自己的專案整輪共用一個 session,狀態主要靠 fixture 那篇講的 teardown type 收尾,把 App 帶回首頁。 retry:何時用、何時不該用 pytest rerunfailures 可以讓失敗的測試自動重跑: 重跑 2 次、每次間隔 1 秒。適用情境是 真正的隨機外部因素 :網路瞬斷、第三方 sandbox 偶爾逾時。 但 retry 有幾條硬規則: 不該拿來遮蓋你自己程式的 bug :retry 後才過,代表測試邏輯本身有問題,重跑只是在賭。 retry 次數要記錄 : reruns delay 加上 CI log,持續重跑才通過的測試要列追蹤清單。 持續 flaky 的必須查 :如果一條測試每次 CI 都要重跑第 2 次才過,它已經是一個訊號,不是「可以接受的噪音」。 quarantine:隔離而非刪除 已知不穩的測試不該刪,也不該擋住主線 CI。正確做法是用 mark 打標籤,隔離進獨立排程。 CI 主要 gate 排除這些測試: 獨立排程(例如每天一次)單獨跑 quarantine 群: quarantine 只是暫存區。每條打標的測試要開 ticket 追蹤原因和修復期限,否則它會永遠住在那裡。 我被 retry 騙過一次 上面說 retry 次數要記錄。這句話我是吃過虧才懂的。 我的 pytest.ini 裡,addopts 一直帶著 reruns=1。意思是 所有測試 失敗後都會自動重跑一次。「第一次掛、第二次過」的測試,在報表上就是綠的。 有一陣子報表幾乎全綠,我卻覺得怪怪的。因為我有在看測試跑的時候手機的畫面和截圖,畫面上明明有操作失敗、重來的樣子,跟全綠的報表對不上。我去翻 log,才發現很多測試是重跑之後才過的。 那一刻我才意識到兩件事: 第一, 我根本不知道真實的 flaky rate 是多少。 報表只告訴我最後過了沒,沒告訴我過得多辛苦。 第二, 我自己的規則是裝飾品。 框架裡有一條規則「新測試預設加 @pytest.mark.flaky(reruns=1)」,當時大部分測試都沒加,只有少數幾個有。但全域早就在重跑了,加不加根本沒差。 這件事給我的教訓是:報表是給人看的摘要,不是事實本身。偶爾還是要看一下測試實際在做什麼。 沒有量測,就不可能改善 發現問題之後,我沒有馬上把 reruns=1 拿掉。拿掉的話,報表一夕之間會變得很紅,每一個紅燈都要有人看,我一個人看不完。 我先做的是 把被藏起來的東西量出來 。我寫了一個 flaky 追蹤器,接在 conftest 既有的 hook 上: pytest rerunfailures 會把中途失敗的那次標成 rerun,追蹤器據此計數 只記兩種結果: 重跑後才過 (這就是被藏起來的債)和 重跑後還是掛 (真的壞了) 一次就過的不記 每筆記下裝置、平台、OS 版本、環境、build 編號,存成一行一筆的 JSONL 追蹤器所有對外的函式都會吞掉例外, 收集資料的程式不准弄壞測試執行 再配一支報表腳本,列出重跑後才過次數最多的前 10 名。 裝置欄位是我覺得最有價值的設計。同一支測試,如果在所有裝置上都不穩,是腳本的問題;如果只在某一台上不穩,多半是那台裝置或環境的問題,不用改腳本。 我定了節奏,但還沒做到 有了排行榜,我給自己定了一個節奏: 每週看一次排行榜,只修前 10 名,而且只修連續兩週都在榜上的。 偶爾閃一次的不理。flaky 本來就有隨機性,追那種一週只出現一次的,會把整週的時間都吃掉,最後什麼都沒修好。連續兩週都在榜上的,才是真的有問題、修了會有感的。 老實說,這個節奏我還沒有真的照著做。量測的工具做好了,規則也寫下來了,但「每週固定看一次」這件事,還沒排進我的日常。 我把這段留在文章裡,是因為我覺得這才是 flaky 最難的地方。寫追蹤器是一次性的工作,一個下午就做完;固定去看、固定去修,是每週都要付的成本,而且永遠會有更急的事排在它前面。如果你也在做一樣的事,我的建議是:工具做好的那天,就把「每週看一次」排進行事曆,不要像我一樣,想著之後再說。 回頭看,這整件事最重要的一步,是那天覺得「全綠怪怪的」,去看了一眼手機畫面。