iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Software Development

從脆弱腳本到可信任測試平台:自動化測試架構30天系列 第 12

Day 12|不要再亂用固定等待時間!顯式等待、條件式等待與 Timeout 實戰設計

  • 分享至 

  • xImage
  •  

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() 是自動化測試的毒瘤?

我們來算一筆簡單的案例:

  1. 時間浪費(Worst-case Execution):假設頁面載入平均只需 0.3 秒,但你為了保險起見寫了 time.sleep(3)。如果有 100 個案例,每個案例用了 5 次 time.sleep(3),你的 CI 每次執行就會無謂地多浪費 25 分鐘
  2. 依然會 Flaky(Not Guaranteed):當 CI 伺服器 CPU 飆高、網路稍微抖動,頁面載入時間變成 3.2 秒時,你的 time.sleep(3) 依然會跳 Timeout 失敗。

黃金法則:自動化測試的等待,應該是「等待系統進入可驗證狀態(Waiting for State)」,而不是「等待時間流逝(Waiting for Time)」。

二、 剖析 4 種等待機制:你用對了嗎?

┌───────────────────────────────────────────────────────────────┐
│                      4 種等待機制對比                           │
├───────────────────────────────────────────────────────────────┤
│ 1. 固定等待 (Fixed Sleep)     ──▶ 無腦阻塞,極度浪費時間           │
│ 2. 隱式等待 (Implicit Wait)   ──▶ 全域設定,彈性差,易產生副作用    │
│ 3. 顯式等待 (Explicit Wait)   ──▶ 輪詢監聽特定 DOM / 狀態         │
│ 4. 自動等待 (Auto-waiting)    ──▶ 現代工具 (Playwright) 內建     │
└───────────────────────────────────────────────────────────────┘

1. 固定等待(Fixed Sleep)

  • 原理:強制讓 Python 直譯器進程休眠(Block)指定秒數。
  • 評價強烈不建議使用(除非測試目標本身就是時間相關,如驗證 Session Timeout)。

2. 隱式等待(Implicit Wait - 傳統 Selenium 常見)

  • 原理:設定全域 Timeout(例如 10 秒)。尋找任何元素時,如果不存在就每隔 0.5 秒輪詢一次,直到超時。
  • 缺點:只針對「元素出現在 DOM 樹中」有效。但現代 Web 的問題通常是「元素已經在 DOM 裡,但還不可點擊(Not Interactable)」或「元素動畫還在跑」,隱式等待對此無能為力。

3. 顯式等待(Explicit Wait / Condition-based Wait)

  • 原理:明確指定條件(例如:等待特定元素可被點擊、等待特定 API 回傳 HTTP 200、等待 Spinner 消失)。條件滿足就立刻繼續執行。
  • 優點:極度精準、不浪費一毫秒。

4. 自動等待(Auto-waiting / Web-First - 現代 Playwright / Cypress)

  • 原理:在執行 click()fill() 等操作前,工具底層自動進行一系列 Actionability Checks(包含:可見、未被遮蔽、已啟用、動畫已停止等)。

三、 Python / pytest 實戰:從脆弱等待重構成條件式等待

我們來看幾段實務中最常見的情境與重構範例:

情境 1:等待非同步 API 請求完成與 Spinner 消失

❌ 脆弱的寫法(使用 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()

情境 2:等待動態 DOM 或元件狀態(如動畫結束/按鈕啟用)

❌ 脆弱的寫法:

Python

# 點擊下拉選單後,sleep 1 秒等選單展開動畫跑完
page.get_by_test_id("dropdown-menu").click()
time.sleep(1)
page.get_by_test_id("option-item-1").click()

⭕ 穩定的寫法(利用 Playwright 狀態條件):

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()

四、 第一線 QA 的 Timeout 分層設計架構

除了摒棄 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):
    ...

五、 第一線 QA 的心法總結

  1. 搜出專案裡的 time.sleep() 並消滅它們
    打開你的 CI Log 或搜尋程式碼中的 time.sleep。每一次你刪除一個 time.sleep() 並替換為 expect()wait_for(),你的測試就朝著「可信任」又跨進了一大步。
  2. 把「等待」當成測試斷言的一部分
    等待 Spinner 消失、等待特定的 Response 200,這些本身就是產品商業邏輯(Performance / UX)的一環。把它們寫成明確的條件,能幫你第一時間抓出前端效能變慢或 API 卡住的缺陷。

明日預告

解決了「固定等待」這個不穩定因素後,下一個影響 UI 自動化測試生存率的關鍵問題就是:「定位器(Locators)該怎麼設計?」

當前端改版、DOM 結構重新繪製時,你的定位器會不會一夕之間全壞?

明天 Day 13,我們就來探討:《 Day 13|如何設計比較穩定的定位器:從 XPath 陷阱到 data-testid 實戰 》


上一篇
Day 11|為什麼UI自動化測試這麼容易壞?剖析 Web 測試脆弱的 8 大根源
系列文
從脆弱腳本到可信任測試平台:自動化測試架構30天12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言