iT邦幫忙

2026 iThome 鐵人賽

DAY 13
1
Software Development

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

Day 13|如何設計比較穩定的定位器:從 XPath 陷阱到 data-testid 實戰

  • 分享至 

  • xImage
  •  

一個糟糕的定位器,就像把房子蓋在流沙上——前端工程師只要改個 CSS 樣式或是調整一下 HTML 結構,你的測試案例就會在一夜之間全亮紅燈。」

大家好,我是 Jane,一個每天在 第一線跟 DOM 樹、CSS Selector 與前端 UI 改版奮戰的自動化測試工程師(SDET)。

在昨天 Day 12 中,我們消滅了導致測試不穩定的第一大毒瘤 time.sleep(),學會了如何優雅地使用條件式等待。

今天我們要來解決第二大毒瘤:脆弱的元素定位器(Fragile Locators)

在日常排查 CI 失敗時,我們最常看到的錯誤之一就是:TimeoutError: locator.click: Target closedElement not found。很多時候去打開網頁檢查,發現功能明明沒壞,只是前端改版時:

  • <button class="btn-primary"> 改成了 <button class="btn-main">
  • 在外層多包了一層 <div class="wrapper">
  • 動態生成的 ID 從 #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

1. 第一優先:專用測試屬性 (data-testid, data-cy, data-test)

  • 原理:專門為了自動化測試與 E2E 測試保留的 HTML 屬性(如 <button data-testid="submit-btn">)。
  • 優勢與 UI 樣式、HTML 結構、多國語系完全解耦! 無論前端把 CSS 從 Tailwind 改成 Bootstrap,或是把文字從「送出」改成「Submit」,定位器都依然堅固。

2. 第二優先:無障礙與語意化屬性 (Accessibility / Role & Label)

  • 原理:使用 HTML5 的 ARIA 角色與標準無障礙屬性,如 get_by_role("button", name="登入")get_by_label("使用者名稱")
  • 優勢:貼近真實使用者感知的操作方式(使用者是看到「按鈕」和「文字」才去點擊,而不是看 CSS Class)。同時還能順便驗證網站的無障礙(Accessibility)規範。

3. 最低優先/禁止:絕對 XPath 與自動生成的 Class/ID

  • 絕對 XPath/html/body/div[1]/div[2]/main/form/div[3]/button
  • 自動生成 Class.css-1x2y3zbutton-active__3kL2
  • 致命傷:只要頁面多加了一個 <div/,絕對 XPath 就立刻死給你看;每次前端重新打包,自動生成的 Class/ID 就會全變。

二、 Python / pytest 實戰:定位器抗脆性重構

我們用 Playwright (Python) 的語法,來看幾組「脆弱 vs. 穩定」的程式碼對比:

範例 1:登入表單輸入框與按鈕

❌ 脆弱的寫法(依賴 DOM 結構與 Class):

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

範例 2:動態表格與複雜列表中特定列(Row)的定位

在處理表格時,新手常寫出 table > tbody > tr:nth-child(3) > td:nth-child(5) > button。如果表格資料排序改變,點到的就是錯的人。

⭕ 穩定的寫法(利用 Locator 鏈式過濾與 Context 鎖定):

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

三、 第一線 QA 實務:如何在團隊中推行 data-testid

看到這裡,你可能會說:「Jane,data-testid 聽起來很棒,但我們公司的前端程式碼又沒有寫 data-testid,我該怎麼辦?」

這正是第一線自動化 QA 展現溝通價值的時候!推動 data-testid 規範的 3 個步驟:

  1. 制定簡單的命名規範(Naming Convention)
    與前端團隊約定簡單易懂的格式,例如:
    • 輸入框:data-testid="{module}-{field}-input" (例: login-username-input)
    • 按鈕:data-testid="{module}-{action}-btn" (例: checkout-submit-btn)
    • 容器/卡片:data-testid="{module}-{name}-card" (例: product-item-card)
  2. 說明對開發團隊(Dev)的好處
    告訴前端工程師:「加上 data-testid 後,你們隨便重構 CSS、換 UI Framework、調整 DOM 結構都不會再弄壞我的自動化測試,我也就不會因為 CI 假的失敗去打擾你們排查了!」
  3. 主動發 PR 幫忙加,或透過工具自動生成
    如果前端太忙,QA 在熟悉專案前端架構的前提下,可以主動開 PR 幫關鍵元件補上 data-testid;或者在 CI/CD 中設定 React/Vue 的 Babel/Vite 套件,在 build 時自動抓取元素名稱生成。

四、 第一線 QA 的心法總結

  1. 遠離絕對 XPath 與純 CSS 層級順序
    任何包含 nth-child(5)/html/body/div[2] 的定位器,都是潛在的定時炸彈。
  2. 從「使用者感知」的角度選定位器
    如果沒有 data-testid,優先使用 get_by_role()get_by_label()get_by_text()。這能讓你的測試程式碼像使用者手冊一樣容易閱讀。
  3. 定位器應該鎖定「商業意圖」,而不是「視覺呈現」
    點擊的是「送出訂單按鈕」,而不是「右下角的藍色圓角按鈕」。保持定位器與視覺樣式解耦,是測試框架能夠長治久安的關鍵。

明日預告

設計出了穩定的等待條件與定位器後,UI 自動化測試的抗脆性已經提升了 80%。但剩下那 20% 最頑固、最讓人崩潰的問題,就是著名的 Flaky Test(忽真忽假測試)

當測試在本地跑 10 次全過,放到 CI 上跑 10 次卻隨機失敗 2 次時,我們該怎麼排查與分類?

明天 Day 14,我們將來到整個系列文章最具實務價值的一篇:
《 Day 14|Flaky test 到底該怎麼處理?建構排查分類與治理機制 》


上一篇
Day 12|不要再亂用固定等待時間!顯式等待、條件式等待與 Timeout 實戰設計
下一篇
Day 14|Flaky test到底該怎麼處理?建構排查分類與治理機制
系列文
從脆弱腳本到可信任測試平台:自動化測試架構30天14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言