「
time.sleep()就像是解決塞車問題時,選擇叫大家在路上把引擎關掉靜止 5 分鐘——它沒有解決 congestion,只是讓整條 Pipeline 變得又慢又不可預測。」
大家好,我是 Jane,一個每天在 第一線跟各種非同步 UI、DOM 渲染與 Timeout 奮戰的 自動化測試工程師(SDET)。
在上集 Day 11 中,我們聊到了導致 UI 自動化測試脆弱的 8 大根源,其中排名第一、最常被新手工程師濫用的「毒瘤」,就是硬編碼的固定等待時間(Fixed Sleep / Magic Sleep)。
當我們看到腳本因為「元素還沒出現」而跳錯時,最直覺的動作往往是:
Python
# 看到報錯 -> 加上 time.sleep(3) -> 沒錯了!大功告成?
time.sleep(3)
然而,這種「加個 3 秒試試看」的習慣,正是讓你的 CI Pipeline 從 5 分鐘膨脹到 1 小時、測試案例從綠燈變成 Flaky(忽真忽假)的罪魁禍首。
今天這篇文章,我們就來徹底拆解:為什麼固定等待是個陷阱?各種 Wait 機制有何不同?以及如何在 Python / pytest 中設計出優雅又穩定的等待條件!
time.sleep() 是自動化測試的毒瘤?我們來算一筆簡單的案例:
time.sleep(3)。如果有 100 個案例,每個案例用了 5 次 time.sleep(3),你的 CI 每次執行就會無謂地多浪費 25 分鐘!time.sleep(3) 依然會跳 Timeout 失敗。黃金法則:自動化測試的等待,應該是「等待系統進入可驗證狀態(Waiting for State)」,而不是「等待時間流逝(Waiting for Time)」。
┌───────────────────────────────────────────────────────────────┐
│ 4 種等待機制對比 │
├───────────────────────────────────────────────────────────────┤
│ 1. 固定等待 (Fixed Sleep) ──▶ 無腦阻塞,極度浪費時間 │
│ 2. 隱式等待 (Implicit Wait) ──▶ 全域設定,彈性差,易產生副作用 │
│ 3. 顯式等待 (Explicit Wait) ──▶ 輪詢監聽特定 DOM / 狀態 │
│ 4. 自動等待 (Auto-waiting) ──▶ 現代工具 (Playwright) 內建 │
└───────────────────────────────────────────────────────────────┘
click()、fill() 等操作前,工具底層自動進行一系列 Actionability Checks(包含:可見、未被遮蔽、已啟用、動畫已停止等)。我們來看幾段實務中最常見的情境與重構範例:
time.sleep):Python
# tests/test_checkout.py
import time
from playwright.sync_api import Page
def test_checkout_brittle(page: Page):
page.get_by_test_id("pay-btn").click()
# 盲目猜測付款 API 與轉頁需要 5 秒
time.sleep(5)
assert page.get_by_test_id("success-message").is_visible()
Python
# tests/test_checkout.py
from playwright.sync_api import Page, expect
def test_checkout_stable(page: Page):
# 1. 使用上下文監聽特定的背景 API 請求 (Expect Response)
with page.expect_response("**/api/v1/checkout") as response_info:
page.get_by_test_id("pay-btn").click()
# 確保 API 回傳 Status Code 200
assert response_info.value.status == 200
# 2. 顯式等待 Loading Spinner 消失 (Wait for Detach/Hidden)
page.get_by_test_id("loading-spinner").wait_for(state="hidden", timeout=10000)
# 3. 使用 Web-first Assertion (內建自動重試與等待)
expect(page.get_by_test_id("success-message")).to_be_visible()
Python
# 點擊下拉選單後,sleep 1 秒等選單展開動畫跑完
page.get_by_test_id("dropdown-menu").click()
time.sleep(1)
page.get_by_test_id("option-item-1").click()
Python
# 直告指定等待該選項為 visible 且可點擊狀態
option = page.get_by_test_id("option-item-1")
page.get_by_test_id("dropdown-menu").click()
# 顯式確保元素達到可操作狀態
option.wait_for(state="visible")
option.click()
除了摒棄 time.sleep(),在 pytest 框架中設計合理的 Timeout 分層 也是抗脆性的關鍵:
Python
# pytest.ini 或是 config/settings.py
"""
建議的 Timeout 階梯設定:
1. Action Timeout (單一 Click/Fill): 5,000 ms (5秒)
-> 超過 5 秒點不到按鈕,代表前端很有可能有 Bug 或卡住。
2. Navigation / Expect Timeout (轉頁與 Web 斷言): 10,000 ms (10秒)
-> 給予非同步 API 讀取合理的緩衝。
3. Test Case Overall Timeout (整條案例總時長): 60,000 ms (60秒)
-> 避免無效的無窮迴圈掛死 CI Runner。
"""
在 pytest 中可以使用 pytest-timeout 套件保護整條案例:
Python
import pytest
@pytest.mark.timeout(30)# 限制此案例最多執行 30 秒,超過強制終止
def test_complex_flow(page):
...
time.sleep() 並消滅它們time.sleep。每一次你刪除一個 time.sleep() 並替換為 expect() 或 wait_for(),你的測試就朝著「可信任」又跨進了一大步。解決了「固定等待」這個不穩定因素後,下一個影響 UI 自動化測試生存率的關鍵問題就是:「定位器(Locators)該怎麼設計?」
當前端改版、DOM 結構重新繪製時,你的定位器會不會一夕之間全壞?
明天 Day 13,我們就來探討:《 Day 13|如何設計比較穩定的定位器:從 XPath 陷阱到 data-testid 實戰 》。