表單是測試人員的主場。你閉著眼睛都知道要試空值、格式錯誤、超長輸入。這篇不教新的測試觀念,教的是翻譯:把你腦中那套表單直覺,變成 AI 寫得出來的具體指令。
文中的例子跑在公開練習站 the-internet 的登入頁,網址 https://the-internet.herokuapp.com/login,帳號 tomsmith、密碼 SuperSecretPassword!,頁面上自己就寫著。你可以照著跑,看得到紅綠。這張表單只有兩個欄位,涵蓋不到的場景後面會另外列出來。
文中的錯誤訊息都是實際觸發一次抄下來的,不是猜的。這件事在這篇不是細節,是主張:訊息文字只要有一個字不一樣,斷言就會紅。你自己的系統也一樣,先跑一次再寫。
先把場景地圖攤開

圖 1:表單測試的六大場景分類
業界的表單再怎麼變,測起來大致就這六類。多數人手動測到第二類就停了,不是不會測後面四類,是手動測太麻煩:要開兩個分頁、要造一筆會撞名的資料、要精準連點兩下。這些正好是自動化最划算的地方。
練習站的登入頁碰得到的是第一類、第六類,以及一部分的第五類。剩下的要回你們自己的系統測,文章後段會單獨列。
手動檢查點,逐條翻成斷言

表 1:手動檢查點與斷言描述的對照
這張對照表有個規律:每個手動檢查點,都對應「一組動作 + 一個畫面上看得到的現象」。
翻譯的功夫全在把現象說清楚。「登入失敗要提示」不夠,「表單上方出現 Your username is invalid!」才是斷言寫得出來的描述。第 5 篇那個檢查法再用一次:預期結果要能拍張截圖圈出來。
驗證時機決定動作順序

圖 2:三種驗證時機,對應三種動作順序
練習站的登入頁是最單純的那一種:按下 Login 才驗證,打字過程中什麼都不會跳。所以測試的動作順序很直接,填完兩欄、按鈕、驗訊息。
你們自己的系統多半沒這麼單純。實務上混合型最多:格式在離開欄位時驗,必填留到送出才驗。onBlur 的表單,你只輸入不移開焦點,訊息永遠不會出現,測試就會掛在「找不到錯誤訊息」。先手動玩一遍確認行為,寫進指令,不要讓 AI 猜。
切類別的判準:行為相同,不是錯誤原因相同
在把場景寫成清單之前,先做一次等價類分析。定義只有一句:落在同一類的值,系統對它們的行為相同,所以挑一個代表值測,等於測了整類。有效與無效只是最容易舉例的一種切法,不是切法的全部。
判準要放在「系統拿它怎麼辦」,不是「它為什麼錯」。帳號留空、打 foobar、打一串亂碼,錯的原因不一樣,但登入頁通通回同一則 Your username is invalid!。行為相同就是同一類,挑一個測就好。
有效類本身也能繼續往下切。帳號打 tomsmith 是有效的,但密碼錯跟密碼對的結果完全不同,一個停在原頁、一個進 /secure。這是兩類。
順帶一提,這個站帳號錯和密碼錯給的是不同訊息,所以兩類切得開。很多產品為了不讓人拿登入頁列舉帳號,會故意兩種都回同一句「帳號或密碼錯誤」——那時候這兩類的行為就變成一樣,該併成一類。同樣一張登入頁,設計不同,類別數就不同。
圖 3:有效類不是終點,行為不同就再切一刀
被切分的對象也不一定是單一欄位。登入頁的類別切在 (帳號, 密碼) 這一組上,單看密碼欄位,wrongpass 本身沒有任何問題,它錯只錯在跟帳號的搭配。一欄一欄切輸入域,永遠切不出這一刀。

表 2:登入頁的等價類切分
這張表有個結論對後面寫指令直接有用:類別數不等於場景數。兩欄都留空跟帳號打 foobar,訊息一模一樣,那是同一類,清單上有一條是多的——留著只是為了讓你看到「同類挑兩個代表值也不會多測到東西」。
邊界值分析接在這後面。登入頁沒有長度規則,所以這篇用不到;等你回自己系統測密碼欄位,先切出「1 到 7 碼」和「8 碼以上」兩類,7 跟 8 這條邊界才存在。等價類決定要測幾類,邊界值決定每類挑哪個值。
有兩種場景切不進這張表:連按兩次送出、以及訊息出現後要消失。它們的行為差異來自時序和前一個狀態,同樣的值換個順序結果就不同,實務上畫狀態轉移圖比硬套等價類好使。
反向場景才是重頭戲
圖 4:一句籠統的指令,產出會偏向正向流程
AI 有個可預期的偏向:你說「幫我測登入頁」,它產出的多半是 tomsmith 登入成功那一條,反向場景蜻蜓點水。
這不是它偷懶,是「測登入頁」這句話本身沒把反向場景點出來。所以完整的表單指令,長得像一份場景清單:
你可以這樣對 Claude Code 說:
測試 https://the-internet.herokuapp.com/login,欄位:Username、Password,送出鈕文字 Login。
驗證時機:按下 Login 才驗證,沒有即時驗證。
正向場景:
1) tomsmith / SuperSecretPassword! → 導向 /secure,出現「You logged into a secure area!」,
畫面上有 Logout 連結。
反向場景(每一條都要額外驗證:網址仍停留在 /login):
2) 兩欄都留空 → 出現「Your username is invalid!」。
3) foobar / barfoo → 出現同一則「Your username is invalid!」。
4) tomsmith / wrongpass → 出現「Your password is invalid!」。
5) 「 tomsmith 」(前後各一個空白)/ 正確密碼 → 出現「Your username is invalid!」,不會自動清掉空白。
其他場景:
6) 登入成功後點 Logout → 回到 /login,出現「You logged out of the secure area!」。
7) 未登入直接開 /secure → 被導回 /login,出現「You must login to view the secure area!」。
注意:訊息區塊裡含有關閉用的 ×,比對訊息請用「包含」,不要用完全相等。
這份清單就是上一節那張表換個輸出格式:每個類別挑一個代表值,加上邊界上那幾個非挑不可的。
每個反向場景後面那句「網址仍停留在 /login」值得單獨講。錯誤訊息有出現、人卻照樣被放進去了,是真實世界常見的 bug 形態。只驗訊息,會放它過關。
清單給出去之後,程式回來長這樣
把這份清單貼給 Claude Code,它會產出一個 tests/login.spec.ts,裡面七個場景對應七個 test 區塊。挑第 3 條那個區塊來看,翻成程式碼是這個形狀:
// tests/login.spec.ts 裡的其中一個 test 區塊
import { test, expect } from '@playwright/test';
test('帳號不存在 → 顯示錯誤訊息,且停留在登入頁', async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/login');
await page.getByRole('textbox', { name: 'Username' }).fill('foobar');
await page.getByLabel('Password').fill('barfoo');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.locator('#flash'))
.toContainText('Your username is invalid!');
await expect(page).toHaveURL(/\/login$/);
});
三段對起來看:goto 到 fill 到 click 是動作,toContainText 是畫面上看得到的現象,toHaveURL 是那句「網址仍停留在 /login」的攔截。你清單上交代的三件事,一件都沒掉。
兩個實務細節。密碼欄用 getByLabel 而不是 getByRole,因為 input type=password 沒有對應的 ARIA 角色,getByRole('textbox') 抓不到它;訊息用 toContainText 而不是 toHaveText,因為那條 flash 裡還包著一個關閉用的 ×,完全比對會失敗。
這兩件事都是跑過一次才會知道的。你猜不到,AI 也猜不到。
檔案 Claude Code 會直接寫進 tests/,跑起來:
npx playwright test tests/login.spec.ts
npx playwright test tests/login.spec.ts --ui
第一次跑建議加 --ui,會開一個介面讓你看它實際點了哪裡、每一步畫面長什麼樣。對照著看,很快就會發現哪個場景的描述寫得不夠具體。
紅了先別急著叫 AI 修。測試失敗只有兩種可能:程式抓錯了,或產品真的有 bug。前者要改程式,後者要開單。分不清就丟回去一句「幫我修好」,AI 最省事的修法是把精確文字換成模糊比對、把 toContainText 換成 toBeVisible——測試變綠了,問題還在原地。下一節那三個排查順序,就是在幫你分辨是不是前者。
常見問題:錯誤訊息一直抓不到
表單測試最常見的卡點,是斷言說找不到錯誤訊息,但你眼睛明明看得到。三個常見原因,照順序排查。
一,訊息是動態出現的,測試驗太早。還記得第 8 篇嗎?expect 自帶重試,最多等 5 秒。練習站掛在免費方案上,冷啟動有時要好幾秒,這一點體感會很明顯。
二,畫面上有多個錯誤訊息,定位對到別欄的。描述時把歸屬講清楚:「Email 欄位下方的那條錯誤訊息」,AI 會用範圍限定的方式找,先找到欄位那個區塊,再在裡面找訊息。
三,訊息其實不在你以為的地方。登入頁就是現成的例子:訊息不在欄位旁邊,而是整頁最上方那條橫幅。你照直覺去欄位下方找,永遠找不到。先手動觸發一次錯誤,看清楚訊息實際出現在哪,再描述。眼見為憑,永遠比想像可靠。
這個站沒有,但你們系統一定有
登入頁只有兩個欄位,下面這四種它示範不了。回自己系統測的時候,這幾條的投資報酬率通常最高。
一、連按兩次送出。網路一慢,使用者一定會再按一次。前端沒把按鈕變成不可按、後端又沒防重,結果就是兩筆訂單、兩張工單。這種 bug 手動很難穩定重現,自動化反而容易。
二、前後空白與全形字。使用者從 Excel 或 LINE 複製貼上,字串前後常常帶著空白;中文輸入法沒切回半形,打出來的是全形的 @ 跟全形數字。清單第 5 條那個帶空白的帳號,就是這類場景在練習站上唯一測得到的版本。
三、只有後端擋得住的規則。Email 重複、統一編號檢核碼、庫存不足、額度超限。這類場景前端看起來一切正常,要等回應回來才知道。測的時候要自己準備一筆「一定會撞到」的資料,別依賴環境上剛好存在的東西。
四、錯誤訊息的下半場。格式錯誤訊息出現後,把欄位改正,訊息應該消失。練習站的 flash 只能用 × 點掉,測不到「改正後自動消失」,但你們系統多半有即時驗證,這條一定要補。只驗出現不驗消失,會漏掉「錯誤訊息黏住不放」這種經典 bug。

圖 5:錯誤訊息的完整驗證鏈,前兩段只是開頭
下拉選單、勾選框這些傢伙
練習站剛好都有:/dropdown 是下拉選單,/checkboxes 是勾選框,/upload 是檔案上傳,/dynamic_controls 是按了才會啟用的欄位。不同控件在 Playwright 各有寫法(selectOption、check、setInputFiles 之類),你不用背,描述現象就好:「從下拉選單選 Option 2」「勾選第一個 checkbox,驗證它變成已勾選」。AI 會選對方法。
你的心力該花在行為規則上:不勾同意條款時,送出鈕是「不可按」,還是「按了跳提示」?日期區間裡起日晚於迄日,是當場擋下來,還是自動對調?兩種設計對應不同的斷言。知道你們產品是哪一種的人,是你。
今天的練習
‧ 熱身:在 /checkboxes 練最簡單的驗證流:勾選第一個 checkbox,驗證它變成已勾選、第二個維持原狀。這是「動作 + 現象」的最小版本。
‧ 主菜:把上面那份清單整份交給 Claude Code 跑一次,七條全綠才算完成。跑之前先自己在瀏覽器把七個場景各觸發一遍,對照訊息文字有沒有一字不差——這一步不能省,也不要在跑失敗之後讓 AI 自己去改斷言。
‧ 回自己系統:挑一張真實表單,照表 2 的格式列出類別,再轉成場景清單。至少要有一條是重複資料或連點兩次這類練習站測不到的場景。
‧ 如果手上已有這張表單的手動測試案例:整份貼給 Claude Code,請它「翻成 Playwright 測試,不確定的預期結果標出來問我」。第 28 篇會系統化這個轉換,先試個手感。