「對失敗的測試加
retry: 3,就像是用黑膠帶把發出異音的引擎轉速表貼起來——問題沒有消失,你只是選擇眼不見為淨。」
大家好,我是 Jane,一個每天在第一線處理跨平台(Web & App)自動化測試、跟 CI/CD Pipeline 與 Flaky Test 奮戰的自動化測試工程師(SDET)。
在上集 Day 14 中,我們拆解了 Flaky Test 的 6 大根源,並建立了隔離區(Quarantine)機制。
當許多 QA 面對測試不穩定時,第一時間最常採取的「快捷方式」就是直接在測試框架(如 pytest 或 playwright)設定:
Ini, TOML
# pytest.ini
addopts = --reruns 3 --reruns-delay 2
看似簡單一行設定,CI 上的紅燈立刻少了一半,團隊一片歡騰。
但身為負責品質防線的 SDET,我們必須誠實面對:Retry(自動重試)絕不是解決 Flaky Test 的真答案,盲目使用重試機制,往往是在飲鴆止渴。
今天這篇文章,我們就來好好剖析:重試機制的副作用、哪些情境才適合重試,以及如何設計一套「不掩蓋問題」的重試與隔離策略!
┌─────────────────────────┐
│ Retry 機制的三大副作用 │
└────────────┬────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
1. 掩蓋產品真實 Race Condition 2. CI 執行時間成倍飆升 3. 測試資料與狀態二度污染
許多 Flaky Test 不是測試程式碼寫爛,而是產品前端與後端 API 在極端網路時序下的真實問題(例如:按鈕重複點擊、非同步狀態同步失敗)。如果設了 Retry,第一次失敗但第二次重試成功了,這個能抓到真實 Bug 的訊號就被徹底掩蓋掉了。
假設一個 UI 案例執行需 30 秒,如果框架中有 10 個不穩定的案例,每個案例都重試 3 次,單單這 10 個案例就會多消耗 15~20 分鐘 的 CI 資源!團隊等 PR Merge 的時間被無限拉長。
如果案例在第 3 步「建立訂單」成功後,在第 4 步驗證失敗觸發 Retry;當它重試第 1 步時,資料庫裡已經存在剛才建好的訂單資料,這會直接引發第二次重試因「資料重複」而再次失敗。
並非不能用 Retry,而是要精準區分適用情境:
| 類別 | 情境說明 | 是否允許 Retry? | 處置建議 |
|---|---|---|---|
| 環境與網路瞬斷 | 測試環境 DNS 抖動、第三方金流 API 瞬斷 (503 Service Unavailable) | ✅ 允許 (有限度) | 僅限特定 Network Error 觸發,且需記錄首次失敗 Log。 |
| 產品商業邏輯 | 轉帳、扣款、建立使用者、狀態流轉斷言 | ❌ 絕不 Retry | 一旦失敗即確定為 Issue 或測試邏輯錯誤,需立刻排查。 |
| UI 元素狀態 | 定位器找不到、DOM 狀態未更新 | ❌ 絕不 Retry | 應透過顯式等待(expect / wait_for)解決(Day 12),而非 Retry。 |
如果一定要使用重試機制,絕對不能讓第一次失敗「無痕消失」。我們必須在 pytest 中紀錄首次失敗的資訊,並結合隔離標籤。
pytest-rerunfailures 進行精準重試設定不要在全域開啟無差別重試,改為針對特定外部依賴案例進行標記:
Python
# tests/test_payment_gateway.py
import pytest
from playwright.sync_api import Page, expect
# 僅針對高度依賴外部第三方 Sandbox 的案例允許重試 2 次
@pytest.mark.flaky(reruns=2, reruns_delay=1)
def test_third_party_payment_sandbox(page: Page):
"""
第三方金流服務 Sandbox 極易抖動,此處允許有限度重試
"""
page.goto("https://staging.example.com/checkout")
# 操作第三方金流 ...
expect(page.get_by_test_id("payment-success")).to_be_visible()
conftest.py 中捕捉重試紀錄並保存首次失敗資訊確保每一次 Retry 的第一次失敗截圖與 Log 都有被保留,以供日後排查:
Python
# tests/conftest.py
import pytest
from playwright.sync_api import Page
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
# 檢查該案例是否觸發了 rerun (重試)
if hasattr(report, "rerun") and report.rerun > 0:
print(f"\n[Warning] 案例 {item.name} 正在進行第 {report.rerun} 次重試!")
# 如果重試過程中發生失敗,立刻抓取當下螢幕截圖與 Trace
if report.when == "call" and report.failed:
page: Page = item.funcargs.get("page")
if page:
screenshot_path = f"reports/screenshots/retry_{report.rerun}_{item.name}.png"
page.screenshot(path=screenshot_path)
print(f"[Screenshot] 失敗截圖已儲存至: {screenshot_path}")
比起無腦 Retry,資深 SDET 更傾向採用 Quarantine(隔離機制):
quarantine 標籤。恭喜大家完成了「第三部分:處理最容易讓測試變脆弱的問題(Day 11~15)」!
我們解決了等待時間、定位器、Flaky test 與 Retry 陷阱。從明天開始,我們將跨入全新的:
《第四部分:測試資料、環境與 API 整合》。
明天 Day 16,我們將來探討自動化測試中最硬核的底層問題:
《 Day 16|測試資料為什麼比測試程式更難管理?剖析資料依賴與污染的陷阱 》。