AI Agent 如何協作 Playwright:從需求變更到可驗證的測試閉環

封面圖:AI Agent 與 Playwright Test 協作

當 Playwright 測試已能把固定的使用者操作流程整理成可重複執行的腳本後,下一個問題通常不是「還能再寫哪些 API」,而是:

當需求持續變更時,如何讓 AI Agent 協助擴展、修正與審查測試,同時避免測試品質隨著自動生成而下降?

AI Agent 確實可以快速產生 Playwright 測試,但若只給一句「幫我測試折扣功能」,Agent 仍必須自行猜測帳號、測試資料、操作流程、預期結果與失敗判準。最後很可能得到一支「可以跑」,卻無法確認是否真正驗證需求的測試。

較可靠的方式,是把 Agent 放進一個有明確輸入與可驗證輸出的測試閉環:

需求變更
→ 功能交付
→ Agent 補上或調整 Playwright 測試
→ 審查測試意圖、Locator 與 Assertion
→ 執行測試
→ 檢視錯誤訊息與 Evidence
→ 回饋 Agent 修正
→ 再次執行確認結果

這個流程的核心,不是讓 Agent 一次產生完整測試平台,而是讓它在規格、測試資料、測試結果與失敗證據(Evidence)的約束下工作。

圖:AI Agent 與 Playwright 測試回饋閉環

圖:AI Agent 與 Playwright 測試回饋閉環

AI Agent 可以協助測試,但不能取代測試判斷

在 UI 自動化測試流程中,Agent 可以扮演多種角色:

  • 將需求規格整理成可執行的測試需求。
  • 拆分主流程 Smoke Test 與功能分支測試。
  • 產生 Playwright Test、測試資料與 helper function。
  • 根據失敗訊息、Screenshot、Trace 或 Report 修正腳本。
  • 審查 Locator、Assertion、測試資料與測試意圖。

問題在於,Agent 的「完成」不等於測試真的正確。

例如測試失敗時,最容易出現的錯誤修正方式包括:

  • 刪除失敗的 expect
  • 把精確金額驗證改成只檢查「NT$」
  • 把指定商品改成任意商品
  • 增加 waitForTimeout()
  • 改用脆弱 CSS selector 讓 locator 暫時找得到
  • 直接移除失敗的測試情境

這些做法可能讓測試由紅轉綠,但測試本身已經失去原本的驗證意圖。

因此,Agent 需要有明確使用邊界:

  • 不得自行補完規格未定義的業務規則。
  • 不得為了讓測試通過而弱化 Assertion。
  • 不應只依程式碼推測 UI 狀態。
  • 畫面與規格不一致時,先回報差異,而不是修改測試來迎合現況。
  • 若缺少穩定 Locator,應提出可測試性補強,而不是直接改用脆弱 DOM 路徑。

Playwright 官方文件本身也強調以 Locator、auto-waiting、web-first assertions 與測試隔離建立可維護測試。Agent 協作沒有改變這些基本原則,只是讓它們更需要被明確寫進工作規範。

不要只給 Agent 一句「幫我測試這個功能」

假設一個既有的購物流程原本只有一般會員結帳,現在加入:

  • VIP 會員自動折扣
  • 有效優惠碼折抵
  • 無效優惠碼錯誤訊息
  • VIP 與優惠碼互斥規則

如果只要求:

請幫我測試新的折扣功能。

Agent 仍不知道:

  • 哪個帳號是一般會員、哪個是 VIP。
  • 商品單價與數量是多少。
  • 有效與無效優惠碼分別是什麼。
  • VIP 與優惠碼同時使用時應如何處理。
  • 要驗證折扣名稱、折扣金額,還是只驗證按鈕可點擊。
  • 原本的 Smoke Test 是否應修改。
  • 可不可以直接修改產品功能。

一個較可執行的測試需求,至少應明確包含:

  • 測試目標
  • 規格來源
  • 已交付功能範圍
  • 測試帳號與資料
  • 需要新增或調整的測試案例
  • 主要 Assertion
  • Locator 原則
  • 可修改與不可修改範圍
  • 驗證指令
  • 回報格式

以折扣案例來說,測試資料可以先整理為:

一般會員:normalUser
VIP 會員:vipUser
商品單價:680
數量:2
有效優惠碼:PET100
無效優惠碼:INVALID

規則則明確寫成:

VIP:商品小計 9 折
PET100:一般會員折抵 100 元
INVALID:不折扣並顯示錯誤訊息
VIP + PET100:維持 VIP 折扣,並提示不可同時使用

這樣 Agent 才是在「依規格產生測試」,而不是「看程式碼猜一套測試」。

需求增加,不代表把所有情境塞進同一支 E2E 測試

原本的購物主流程可以是一條 Smoke Test:

登入
→ 選擇商品
→ 加入購物車
→ 進入結帳
→ 確認金額
→ 送出訂單
→ 驗證訂單成立

加入折扣需求後,最直接但不理想的做法,是繼續往這支測試後面追加 VIP、優惠碼、無效優惠碼與互斥規則。

結果通常會變成:

  • 測試越來越長。
  • 多組帳號與資料互相干擾。
  • 失敗時難以定位是哪個規則出問題。
  • Agent 修正其中一段時,容易影響其他段。
  • 測試名稱已無法反映真正的驗證目的。

較合理的做法,是保留原本的主流程 Smoke Test,再新增獨立的功能分支:

基準 Smoke Test
→ 一般會員可以完成基本購物與結帳
折扣分支
→ VIP 會員結帳時自動套用 VIP 折扣
→ 一般會員使用有效優惠碼後折抵 100 元
→ 一般會員使用無效優惠碼後顯示錯誤訊息且金額不變
→ VIP 會員輸入優惠碼時顯示互斥提示並維持 VIP 折扣
圖:需求變更後需拆解測試結構

圖:需求變更後需拆解測試結構

程式結構也可以直接反映這個分工:

tests/
├─ smoke/
│  └─ checkout.spec.ts
└─ discount/
   ├─ vip_discount.spec.ts
   ├─ coupon_discount.spec.ts
   ├─ invalid_coupon.spec.ts
   └─ discount_exclusion.spec.ts

重點不是一定要拆成四個檔案,而是每個 Test Case 應有單一且可辨識的驗證目的

測試資料與預期值不要散落在腳本裡

需求簡單時,直接寫:

await expect(page.getByTestId('checkout-total'))
  .toHaveText('NT$ 1224');

並沒有立即問題。

但當測試開始包含不同會員身分、優惠碼與互斥規則,如果多支測試都散落:

NT$ 1360
NT$ 136
NT$ 1224
NT$ 1260

Agent 後續修改時,很容易只修到其中幾個數字。

較好的方式,是將測試資料與簡單預期值計算集中管理:

export const users = {
  normal: {
    username: 'user1',
    password: 'user1',
  },
  vip: {
    username: 'user2',
    password: 'user2',
  },
} as const;
export const product = {
  unitPrice: 680,
  quantity: 2,
} as const;
export const coupons = {
  valid: 'PET100',
  invalid: 'INVALID',
} as const;

再用一個保持簡單的 helper 推導預期結果:

type MemberType = 'normal' | 'vip';
export function calculateExpectedAmount(
  unitPrice: number,
  quantity: number,
  memberType: MemberType,
  couponCode?: string,
) {
  const subtotal = unitPrice * quantity;
  const vipDiscount =
    memberType === 'vip'
      ? Math.round(subtotal * 0.1)
      : 0;
  const couponDiscount =
    memberType === 'normal' && couponCode === 'PET100'
      ? 100
      : 0;
  return {
    subtotal,
    vipDiscount,
    couponDiscount,
    total: subtotal - vipDiscount - couponDiscount,
  };
}

這裡有一個重要限制:測試 helper 不應完整複製產品端的商業邏輯。

如果產品與測試使用完全相同的折扣實作,產品邏輯寫錯時,兩邊可能一起算出相同的錯誤結果。helper 的用途只是讓測試預期值有單一、可讀、可核對的來源,而不是再做一套產品規則引擎。

Agent 產生測試後,還需要審查 Locator 與 Assertion

Agent 很擅長快速產生「能定位到元素」的 selector,但能找到元素,不代表是好的 Locator。

Playwright 官方建議優先使用具使用者語意、可重試且較穩定的定位方式。實務上可依下列方向檢查:

getByRole
→ getByLabel
→ getByText
→ getByTestId
→ 穩定屬性 selector
→ 結構型 CSS / XPath 作為最後手段

例如:

await page.getByRole('button', { name: '套用優惠碼' }).click();
await expect(
  page.getByTestId('coupon-discount-amount')
).toHaveText('NT$ 100');

不建議 Agent 為了快速通過,改成:

await page.locator(
  '#app > main > div:nth-child(3) > button'
).click();

同樣地,Assertion 也不能只驗證「操作有執行」。

折扣測試至少應視情境驗證:

  • 商品小計。
  • 折扣名稱。
  • 折扣金額。
  • 訂單總金額。
  • 成功或錯誤訊息。
  • VIP / 優惠碼互斥提示。
  • 最終訂單成立頁的金額。

例如有效優惠碼案例,不應只寫:

await expect(page.getByText('PET100')).toBeVisible();

更有意義的是:

await expect(page.getByTestId('coupon-name'))
  .toHaveText('PET100');
await expect(page.getByTestId('coupon-discount-amount'))
  .toHaveText('NT$ 100');
await expect(page.getByTestId('checkout-total'))
  .toHaveText('NT$ 1260');

這樣測試才是在驗證「折扣規則」,而不是只驗證畫面上曾經出現某段文字。

測試失敗後,不要立刻叫 Agent 改程式碼

當 Playwright Test 失敗時,真正有價值的是失敗 Evidence。

常見 Evidence 包括:

  • 終端機錯誤訊息
  • Screenshot
  • Trace
  • Video
  • HTML Report

比較可靠的流程是:

測試失敗
→ 讀取失敗測試與行號
→ 判斷是 action、locator 還是 assertion
→ 檢查 Screenshot / Trace / Report
→ 比對預期與實際 UI
→ 分類問題來源
→ 再決定修正測試或回報產品缺陷
圖:需求變更後需拆解測試結構

圖:測試失敗證據轉成 Agent 失敗摘要

例如:

Expected: NT$ 1260
Received: NT$ 1360

不能立即推論「產品折扣功能壞了」。

仍需確認:

  • PET100 是否真的已成功套用。
  • 畫面是否顯示優惠碼成功訊息。
  • 測試是否取得正確的總金額欄位。
  • 測試資料是否殘留前一次狀態。
  • 規格中的預期值是否被測試 helper 算錯。

Agent 比較適合接收的,不是「測試失敗,請修」,而是已整理的診斷輸入:

失敗測試:
一般會員輸入 PET100 後應折抵 100 元
失敗步驟:
checkout-total Assertion
預期:
NT$ 1260
實際:
NT$ 1360
Evidence:
- 畫面顯示優惠碼 PET100 已套用
- coupon-discount-amount 顯示 NT$ 0
- Trace 顯示套用按鈕已成功點擊
請判斷:
1. 較可能是產品功能、測試腳本或測試資料問題。
2. 是否應修改測試。
3. 若需修改測試,不得降低原有 Assertion。

這樣 Agent 才是在做「根據 Evidence 的診斷」,而不是猜測。

讓 Agent 修正測試時,明確禁止「弱化驗證」

當 Agent 收到失敗測試後,可以直接加入一些負面限制:

不得:
- 刪除折扣相關 Assertion
- 將精確金額改成模糊文字比對
- 移除 VIP、有效優惠碼、無效優惠碼或互斥測試
- 使用 waitForTimeout() 掩蓋同步問題
- 使用脆弱 CSS selector / XPath 取代穩定 Locator
- 修改產品功能來配合測試

並要求修正完成後重新執行:

npx playwright test tests/discount \
  --project=chromium \
  --workers=1

最後回報:

修改了哪些測試
實際執行的指令
哪些測試通過
哪些仍失敗
是否有新的失敗
使用了哪些 Evidence
是否需要回報產品功能差異

Agent 回覆「已修正」沒有驗證價值;重新執行後的測試結果才有。

長提示詞不是終點:把固定流程移到 AGENTS.md 與專案規範

如果每次要求 Agent 產生或修正 Playwright 測試,都要重新貼上:

  • Locator 原則
  • Assertion 原則
  • 測試資料規則
  • 禁止修改範圍
  • 失敗 Evidence 格式
  • 重新驗證方式
  • 回報格式

代表這些內容已經不是「這次任務的提示詞」,而是專案固定工作規範

此時可以將規範拆出:

project/
├─ AGENTS.md
└─ .agent-rules/
   ├─ project-structure.md
   ├─ tech-stack.md
   └─ playwright-qa-workflow.md

其中 playwright-qa-workflow.md 可以固定記錄:

  • 測試任務必讀哪些規格
  • Agent 可修改與不可修改的目錄
  • Smoke Test 與分支測試拆分原則
  • Locator 優先順序
  • Assertion 最低要求
  • 測試資料與 helper 管理方式
  • 測試執行指令
  • Evidence 回報格式
  • 禁止弱化驗證的規則

AGENTS.md 則只需要成為入口索引:

## 分離規範
- 專案目錄結構:`.agent-rules/project-structure.md`
- 技術堆疊與實作邊界:`.agent-rules/tech-stack.md`
- Playwright QA 工作流程:`.agent-rules/playwright-qa-workflow.md`
涉及 Playwright 測試生成、修正、審查、
執行或失敗分析時,必須讀取
`.agent-rules/playwright-qa-workflow.md`。

之後單次提示詞便可以縮短成:

請依 AGENTS.md 與 Playwright QA 規範, 根據指定規格補上折扣測試。 先提出測試設計,不要直接修改檔案。

固定規則由專案文件承載,提示詞只描述這次任務的差異。

圖:從提示詞操作到專案規範

圖:從提示詞操作到專案規範

從「AI 幫忙寫測試」走向「可驗證的 Agent 測試流程」

AI Agent 最容易展現價值的地方,是加速重複且結構化的工作,包括測試案例擴展、腳本生成、Locator 整理、Assertion 審查與失敗摘要。

但要讓這些能力真正進入開發流程,需要先建立幾個基本約束:

  • 規格先於生成
  • 測試資料先於猜測
  • Evidence 先於修正
  • 驗證結果先於「已完成」
  • 專案規範先於超長提示詞

Playwright 在這裡不只是瀏覽器自動化工具。

當測試案例具備明確意圖、穩定 Locator、具體 Assertion、可觀察 Evidence 與可重複執行的驗證流程後,它就能成為 Agent 任務的可執行驗收條件。

而 AI Agent 的價值,也不再只是「幫忙產生測試程式碼」,而是參與:

需求理解
→ 測試設計
→ 測試實作
→ 執行驗證
→ Evidence 診斷
→ 修正
→ 再驗證

這才是 AI Agent 與 Playwright 協作真正值得標準化的部分!

留下第一條留言