iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

❯❯ 別讓改個 CSS 就全紅!如何寫出不被 UI 樣式牽著走的 E2E 測試
Day 16 測試怎麼寫,才對得起「合約」兩個字

📍 流水線位置|入口 → flow → 型別 → mock → 【測試】 → UI → vibe → 上線(合約的寫法)

流水線位置|入口 → flow → 型別 → mock → 【測試】 → UI → vibe → 上線(合約的寫法)

寫過 E2E 測試的人多半有這個經驗:只是改個間距、調個圓角、換個 class 名稱,測試紅了一片,但業務邏輯明明一行都沒動。

這類問題通常是因為測試腳本直接斷言(Assert)具體的 DOM 階層結構或特定 CSS Class 名稱,導致測試與視覺表現過度耦合。當這種低品質的訊號頻繁出現,就像「狼來了」演變到最後,團隊看到紅燈的第一反應不再是「壞了什麼」,而是「又是哪條測試在亂報」。一旦自動化測試失去信任,它作為防禦機制的價值也就蕩然無存。

前一篇談過綠燈的邊界取決於「驗證範圍怎麼定義」;這一篇則聚焦在斷言的切入層級:如何讓測試精準鎖定領域不變量(Business Invariants),同時又不與視覺表現層緊密綁定。


畫面細節耦合與訊號品質

跟 UI 長相過度耦合的測試,最明顯的特徵就是對「非業務關鍵」的細節下硬斷言:要求某個 Class 名稱必須存在、DOM 節點必須維持特定的階層結構,或是介面文案必須一字不差。

脆硬斷言 vs 語意合約測試

這種測試最大的問題是維護成本。隨手調個排版、改動非關鍵文案,測試就掛給你看;這時我就得放下手邊的工作去修正腳本,只為了讓它配合畫面現況。改久了,大家就會開始想繞過測試。

自動化測試的價值取決於訊號品質,並不是漂亮的覆蓋率數字。測試套件應該在業務邏輯出問題時發出可信的預警,而不是反過來成為介面演進的包袱。


以領域不變量為核心的斷言結構

我們拿「座位管理模組」的實體規格來做個對照。
在 Day 07 的 Flow 文件中,Invariant(領域不變量)明確定義:桌次的識別欄位(tableName)與正常席容量(capacity),必須可被使用者讀取。

※ 註:這裡的「正常席」指的是系統邏輯規則:兒童座椅屬於額外加位,不佔用預設的正常席容量。

對應到 E2E 測試腳本裡,實體斷言長這樣:

await page.getByLabel(/桌次名稱|名稱/).fill('主桌2')
// ...填寫容量與座標設定、架設 API Spy 監聽、發送表單並驗證 Payload
const entity = findEntity(page, /主桌2/)
await expect(entity).toBeVisible()
await expect(entity.getByText(/10/)).toBeVisible()

我們逐段拆解這幾行程式碼,看看它的斷言究竟落在哪個抽象層級:

1. getByLabel(/桌次名稱|名稱/)
使用表單欄位的「業務語意標籤」來定位元素,且正則刻意保持彈性(桌次名稱|名稱)。不論表單如何排版、欄位順序怎麼調整,測試腳本一概不干涉,它只要求這個業務語意能被找到且能順利填入。

2. findEntity(page, /主桌2/)
呼叫專案封裝好的 findEntity Helper 尋找指定名稱的實體。它底層會依序檢索三種 ARIA Role(rowarticlelistitem)。不管前端畫面要把資料呈現成表格橫列、卡片還是清單項目,測試腳本也都不關心,它只驗證這個領域實體在 DOM 裡存在且可見。

3. getByText(/10/)
直接驗證容量數值 10 能在該實體範圍內被讀取。至於字級大小、文字顏色或擺放位置,完全留給 UI 視覺層自由發揮。

除了 UI 層的語意驗證,這條測試還包含 API 網路層的第二重防線:架設 Spy 監聽 POST /tables 請求,精確驗證 Payload 裡的四項核心資料:名稱(主桌2)、容量(10)、座標(X=100Y=200)。前端畫面呈現可以保有彈性,但網路層送出去的資料結構,必須跟合約一模一樣。


邊界劃分:抓緊業務邏輯,放寬視覺呈現

梳理一下這套斷言策略的「鬆與緊」:我的腳本只死守三項核心指標:實體存在性資料可見性,以及 API 傳輸的精確性。只要這三者任何一項出錯,測試立刻紅燈;除此之外的視覺呈現與操作過程,則全面放寬。

收緊的部分,對應 Flow 文件裡的領域不變量(Invariant);而放寬的部分,正好是沒被凍結的視覺細節。測試腳本做的事,就是把 Flow 文件的文字約束翻譯成可執行的程式碼。

我用一張對映表,把座位模組的合約範圍與斷言策略完整攤開:

Flow 領域不變量(Invariant) Spec 實體斷言(Assertion) 邊界控制策略
桌次識別欄位可被讀取 findEntity(page, /主桌2/) 可見 語意鎖定: 收緊語意與實體存在感,不限呈現形式
正常席容量可被讀取 實體節點內 getByText(/10/) 可見 數值鎖定: 抓緊數值呈現,不限樣式與排版
新增桌次的操作流程可達 getByLabel(/桌次名稱|名稱/) 可填寫且表單可提交 行為鎖定: 抓緊表單可操作性,不限欄位配置
資料寫入邏輯精確傳輸至後端 API Spy 斷言:POST /tables Payload 包含名稱、容量、X、Y 嚴格防守:網路合約毫無妥協空間
位置設定方式(解凍範圍) 操作手法不作硬性斷言;座標數值在 Payload 嚴格驗證 鬆綁過程,鎖定結果:不限輸入方式(拖放/數字),只驗最終座標

座位模組邊界防線

最後一列是邊界劃分的經典範例:
Flow 文件把「位置設定方式(拖放畫布或數字輸入)」歸為彈性範疇,前端要怎麼實作互動都可以;但「最終座標數值」仍是不可妥協的合約,測試會在 API Payload 層精確斷言。

操作手法給予彈性,資料結果絕不商量。

至此,測試合約的撰寫規範與邊界防線已建構完畢。
接下來,該輪到自動化工具鏈上場了!看看 AI 怎麼對著這套凍結的測試防線,將 UI 元件與業務邏輯一步步實作至「全線綠燈」!

🎒 最小一步| 翻你專案的 E2E 測試,找出硬斷言具體 DOM 階層或 CSS Class 名稱的段落,改成用 ARIA Role、Label 或語意文字來斷言。
📎 本篇證據| test/e2e/specs/04-seating.spec.ts(斷言原始碼)對照 spec/e2e-flows/04-seating.flow.md(Invariant 條款段落)


上一篇
Day 15 全綠燈,然後正式站還是炸了!從 3 起線上事故看自動化測試的守備盲區與 CI 防線補強
下一篇
Day 17 讓 AI 對著紅燈,自己把 UI 蓋到綠
系列文
收到自己的紅色炸彈!婚期就是 Deadline:前端一人用 AI 規格流水線,30 天出貨婚禮 SaaS17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言