一個糟糕的定位器,就像把房子蓋在流沙上——前端工程師只要改個 CSS 樣式或是調整一下 HTML 結構,你的測試案例就會在一夜之間全亮紅燈。」
大家好,我是 Jane,一個每天在 第一線跟 DOM 樹、CSS Selector 與前端 UI 改版奮戰的自動化測試工程師(SDET)。
在昨天 Day 12 中,我們消滅了導致測試不穩定的第一大毒瘤 time.sleep(),學會了如何優雅地使用條件式等待。
今天我們要來解決第二大毒瘤:脆弱的元素定位器(Fragile Locators)。
在日常排查 CI 失敗時,我們最常看到的錯誤之一就是:TimeoutError: locator.click: Target closed 或 Element not found。很多時候去打開網頁檢查,發現功能明明沒壞,只是前端改版時:
<button class="btn-primary"> 改成了 <button class="btn-main">
<div class="wrapper">
#input-:r1: 變成了 #input-:r2:
如果你的測試腳本充斥著這類容易隨 UI 改版而失效的定位器,你的自動化測試很快就會失去團隊的信任。
今天這篇文章,我們就來好好聊聊:各種定位器的優缺點與優先級,以及如何在 Python / pytest 中設計出「高抗脆性」的穩定定位策略!
在 Playwright / pytest 的世界裡,並非所有的定位器都是平等的。我們應該依照以下穩定度金字塔來選擇定位器:
┌─────────────────────────┐
│ 定位器穩定度金字塔 │
└────────────┬────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
【最高優先級:專用測試屬性】 【中優先級:語意與無障礙屬性】 【低優先級/禁止:脆弱定位】
1. data-testid / data-cy 2. Role + Accessible Name 4. 絕對 XPath (Absolute XPath)
3. Label / Text / Placeholder 5. 自動生成的 CSS Class/ID
data-testid, data-cy, data-test)<button data-testid="submit-btn">)。get_by_role("button", name="登入") 或 get_by_label("使用者名稱")。/html/body/div[1]/div[2]/main/form/div[3]/button
.css-1x2y3z 或 button-active__3kL2
<div/,絕對 XPath 就立刻死給你看;每次前端重新打包,自動生成的 Class/ID 就會全變。我們用 Playwright (Python) 的語法,來看幾組「脆弱 vs. 穩定」的程式碼對比:
Python
# tests/test_login_brittle.py
from playwright.sync_api import Page
def test_login_brittle(page: Page):
page.goto("https://admin.example.com/login")
# 脆弱點 1: 過度依賴 HTML 層級與父子關係
page.locator("form > div:nth-child(1) > input").fill("admin")
# 脆弱點 2: 使用容易因樣式重構而改變的 Class 名稱
page.locator("input.input-field-primary-v2").fill("password123")
# 脆弱點 3: 使用絕對/相對層級過深的 XPath
page.locator("//*[@id='app']/div/div[2]/form/button[2]").click()
data-testid 與語意化定位器):Python
# tests/test_login_stable.py
from playwright.sync_api import Page, expect
def test_login_stable(page: Page):
page.goto("https://admin.example.com/login")
# 穩定點 1: 使用 get_by_test_id (對應 HTML 的 data-testid="username-input")
page.get_by_test_id("username-input").fill("admin")
# 穩定點 2: 使用 get_by_label (貼近使用者輸入體驗)
page.get_by_label("密碼").fill("password123")
# 穩定點 3: 使用 get_by_role + 語意名稱 (精準定位按鈕)
page.get_by_role("button", name="登入").click()
# 驗證成功登入
expect(page).to_have_url("https://admin.example.com/dashboard")
在處理表格時,新手常寫出 table > tbody > tr:nth-child(3) > td:nth-child(5) > button。如果表格資料排序改變,點到的就是錯的人。
Python
# tests/test_user_table.py
from playwright.sync_api import Page, expect
def test_delete_specific_user(page: Page):
page.goto("https://admin.example.com/users")
# 1. 鎖定特定的表格容器
user_table = page.get_by_test_id("user-data-table")
# 2. 在表格內過濾出「包含 Jane 文字」的那一列 (Row)
jane_row = user_table.get_by_role("row").filter(has_text="Jane")
# 3. 在 Jane 這那一列內部尋找「刪除按鈕」並點擊,絕對不會點錯列!
jane_row.get_by_role("button", name="刪除").click()
# 4. 處理彈出的二次確認 Modal (同樣限制在 Modal 範圍內操作)
confirm_modal = page.get_by_test_id("confirm-modal")
confirm_modal.get_by_role("button", name="確認").click()
# 驗證 Jane 已經從表格中消失
expect(jane_row).not_to_be_visible()
data-testid?看到這裡,你可能會說:「Jane,data-testid 聽起來很棒,但我們公司的前端程式碼又沒有寫 data-testid,我該怎麼辦?」
這正是第一線自動化 QA 展現溝通價值的時候!推動 data-testid 規範的 3 個步驟:
data-testid="{module}-{field}-input" (例: login-username-input)data-testid="{module}-{action}-btn" (例: checkout-submit-btn)data-testid="{module}-{name}-card" (例: product-item-card)data-testid 後,你們隨便重構 CSS、換 UI Framework、調整 DOM 結構都不會再弄壞我的自動化測試,我也就不會因為 CI 假的失敗去打擾你們排查了!」data-testid;或者在 CI/CD 中設定 React/Vue 的 Babel/Vite 套件,在 build 時自動抓取元素名稱生成。nth-child(5) 或 /html/body/div[2] 的定位器,都是潛在的定時炸彈。data-testid,優先使用 get_by_role()、get_by_label() 或 get_by_text()。這能讓你的測試程式碼像使用者手冊一樣容易閱讀。設計出了穩定的等待條件與定位器後,UI 自動化測試的抗脆性已經提升了 80%。但剩下那 20% 最頑固、最讓人崩潰的問題,就是著名的 Flaky Test(忽真忽假測試)。
當測試在本地跑 10 次全過,放到 CI 上跑 10 次卻隨機失敗 2 次時,我們該怎麼排查與分類?
明天 Day 14,我們將來到整個系列文章最具實務價值的一篇:
《 Day 14|Flaky test 到底該怎麼處理?建構排查分類與治理機制 》。