Day 28|測試在 CI 上失敗,我連續猜錯兩次原因
前言
CI 紅了,第一個反應常常是打開測試碼,改到它變綠。先等等,搞不好錯的是產品。今天拿昨天 clean 與 with-bugs 的結果做對照,再看每個失敗留下的證據,決定該找誰處理。
開始之前
一支測試紅掉,可能的原因至少十種:選擇器失效、等待條件不對、測資被別人污染、斷言過時、環境掛了、產品真的回歸、或者它根本就是時紅時綠。
這十種的處置完全不同。選擇器失效要改測試,產品回歸要開單,環境掛了什麼都不該改。弄錯類別的代價不是白做工,是做反了。把產品回歸當成測試過時去改斷言,等於親手把 bug 寫進規格。
還有一個更隱蔽的問題:太早認定原因,後面就容易只找支持自己的證據。 看到錯誤堆疊停在某一行,腦子立刻補完一段故事,然後照著改。改完測試綠了,也未必代表剛才猜對了。
所以這一步只做兩件事:歸類,然後指定誰該修,不動手。
動手試試
Day 27 那次 CI,clean 那格紅了兩次。完整過程如下,這不是編的教案。
第一步,看 traceback 就下結論。 錯誤停在 gotoCart 的 waitForSelector,我認定是 addToCart 把等待失敗用 .catch 吞掉、購物車根本是空的。猜錯了。
第二步,拉 evidence。 error-context.md 的 page snapshot 顯示畫面根本不是應用程式:
heading "Performing security verification"
"This website uses a security service to protect against malicious bots"
Ray ID a2468b7c395bd62d
footer "Performance and Security by Cloudflare"畫面上根本沒有 [data-test="cart-total"],所以逾時。判 environment。
第三步,重跑確認。 gh run rerun --failed 又出現同樣症狀,先把它當成可重現的失敗繼續查。兩次都紅還不足以保證它永遠不會時紅時綠,這個問題明天再細講。
第四步,比對同一個 job 內部。 這一步推翻了第二步:
gotoHome、商品頁、login.spec 都在同一個 runner、同一個出口 IP、
同一分鐘內成功載入,只有 gotoCart 那一步的 page.goto('/checkout') 被擋。這讓調查方向從「整個 CI 環境被擋」縮小到「直接導向 /checkout 這個操作被擋」。我們改查測試的導頁方式,原始紀錄將它歸為 test-defect,意思是這次要修測試。這個判斷還要靠後面的改動與重跑確認,不能只看其他頁面載入成功就說產品一定沒事。
分流到這裡先結束,後面交給負責修測試的流程。為了把故事講完,這裡也放上修復和驗證的結果。
第五步,修。 gotoCart 改成點 [data-test="nav-cart"];順手把 addToCart 的 .catch(() => {}) 拿掉,吞例外把「東西沒加進去」變成兩步之後看不懂的逾時,正是它害第一步猜錯方向。
第六步,驗穩。 修復後的 clean 測試連續 3 次通過,達到這次設定的驗收門檻。這裡的綠燈指的是 clean,不是把刻意植入 bug 的那一格也改到會過。
跑完之後

同一次 run 的另外兩筆走的是完全不同的路。with-bugs 那格畫面正常渲染,cart-total 與 line-price 都在,不是逾時:
Combination Pliers:14.15 × 1 應該等於列總計
Expected: 14.15
Received: 0分類 product-regression,而且跟人工探索與獨立盲驗的結論一致。同一份測試碼、同一次 run,一邊是測試的錯、一邊是產品的錯。
把判斷改過的地方留下來
下面節錄的是當時的紀錄,保留原本使用的欄位與名稱:
nodeid: "e2e/cart-line-total.spec.ts > 每一列 Total 相加,應該等於頁尾的 Total [SUT=clean]"
classification: test-defect # 修正:初判 environment,錯的
original_classification: environment
confidence: high
basis: "……同一個 job 內部:gotoHome、商品頁、login.spec 都成功,只有 page.goto('/checkout') 被擋"
revision: "初判 environment 是錯的,理由是我只看到畫面被擋住就停手……"
fix_target: test-healoriginal_classification 與 revision 這兩欄不是懺悔錄。留著它們,下一輪讀到這一段的人就不會再走一次同一條錯路,而且看得出來這個結論是怎麼被推翻的。這跟 Day 19 那份 dropped_hypotheses 是同一個設計。
不過,這份紀錄也混用了分類層級:test-defect 是「測試有問題」的大類,下面 failure-analysis 的 classification 則要求更細的原因,裡面沒有 test-defect。跟著做時不能直接把舊紀錄當成標準格式;若既有分類沒有涵蓋導頁方式,就先以 unknown 記下這個分類缺口,在 basis 說清楚已查到的原因,再交人確認。不能硬塞成 selector 壞掉,也不能自己增加一個下游不認得的名稱。
一筆與一批也要分清楚:CI 一次紅一片的時候,先把幾十個失敗合併成少數幾個根因群,再對每群分析一次,不要逐筆。上面那次 run 紅 4 筆,合併成 2 群。
背後怎麼做
分類表:每一類直接指定誰該修
| classification | 訊號 | 誰該修 |
|---|---|---|
locator |
找不到元素、選擇器失效 | 改測試 |
wait-timing |
元素還沒好就操作、偶發逾時 | 改測試(改對等待條件,禁固定等待) |
test-data |
帳號被鎖或過期、髒資料 | 隔離或自備測資 |
fixture-isolation |
前一支測試污染了狀態 | 修 fixture 與清理 |
assertion-mismatch |
expected 不等於 actual | 先問 oracle,見下 |
environment |
5xx、DNS、憑證、基礎設施 | 重試加查 infra,不動測試 |
product-regression |
產品真的變了或壞了 | 走產品 bug 流程 |
flaky |
間歇重現,不到 100% | 交明天的定性流程 |
cascade |
前置失敗連鎖導致 | 修根源那一筆,其餘標 cascade |
unknown |
證據不足以歸類 | 補證據,不硬歸類 |
API 測試沒有元素可找、沒有渲染要等,前兩類不會命中,換成三類:contract-drift(回應結構變了)、auth-expired(401 或 403,憑證過期或換環境沒換 token)、rate-limit(429)。判 API 失敗前先看前置請求,取 token、建測資那幾步失敗造成的紅是 cascade,不是被測端點的問題,記成產品迴歸會派錯人。
關鍵分岔:assertion-mismatch
expected 不等於 actual 的時候,不要自己猜產品該不該長這樣。交給 oracle 判:
- 產品應該變成這樣(規格改了)→ 測試過時 → 更新斷言
- 產品不該變成這樣 →
product-regression→ 走 bug 流程,不准改斷言遷就
第二種情況如果靠改斷言讓測試通過,就會把錯誤行為當成正確答案。昨天那篇修復案例也提醒過:若是測試原本就寫錯,必須先提出對照依據,不能只看紅燈就決定改哪一邊。
分類本身也有一條紀律:basis 要指向具體證據檔,不憑印象。上面那個案例第二步的 environment 就是有 basis 的,它仍然錯了,錯在只看單一證據就停手。推翻它的不是更好的直覺,是同一個 job 內部的橫向對照。
今天學到什麼
今天把同一輪的紅燈分開看:乾淨版卡在導頁時的驗證畫面,植入 bug 的版本則真的出現金額不一致。錯誤訊息看起來都叫「失敗」,接下來要處理的事情卻不同。
我先猜購物車沒加成功,後來又太快認定整個環境有問題。把同一個 job 裡成功與失敗的步驟放在一起對照,才縮小到導頁方式。分流完成後再交給修測試的流程,原本猜錯的紀錄也留著,讓下一位同事知道結論怎麼來。
明天處理另一種麻煩:同樣條件下,有時紅、有時綠。這種情況不能只看一次,我們會先決定跑幾次,把失敗次數記下來,再談後續怎麼修。
參考資料
- David J. Agans(2002)《Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems》 - 第二條規則是 Quit Thinking and Look,正是本篇第一步猜錯、第二步拉證據的寫照
- Richard I. Cook(1998)《How Complex Systems Fail》 - 複雜系統的失效通常是多因的,單一根因往往是敘事上的簡化,所以分類要指得到證據
- John Allspaw(2012)《Blameless PostMortems and a Just Culture》,Etsy Code as Craft - 把判錯留在紀錄裡而不是抹掉,組織才學得到東西,對應
revision那一欄 - Gerald M. Weinberg(1985)《The Secrets of Consulting》 - 沒有證據支撐的因果推論有多容易成形,說明為什麼
basis是必填 - 本專案
skills/maintain/failure-analysis/SKILL.md- 十類分類表與 API 三類 - 本專案
output/sessions/2026-08-02_ci-e2e-first-run/failure-analysis.yaml- 含被推翻的初判 - 本專案
tests/broken/- 測試的錯的實測基準