測試也是文件,而且是唯一保證跟系統行為同步的文件——因為它過不了就會叫。但這份文件讀不讀得動,取決於兩件樸素的事:名字取得好不好、東西放得找不找得到。
這篇整理的是業界測試團隊普遍在用的做法,沒有什麼高深技術,靠的是紀律。做法本身十分鐘就學得會,難的是每次都做到。
一、為什麼命名值得花這個力氣
「test1」「登入測試」這種名字,平時沒感覺,出事就知道痛。CI 半夜紅了一顆,通知上只寫「登入測試 failed」——所以是哪個場景壞了?錯誤密碼的?帳號被鎖的?你得打開程式碼考古。
對照好的命名:「登入:輸入錯誤密碼 → 顯示錯誤訊息且停留在登入頁」。光看名稱就知道壞掉的行為是什麼、嚴不嚴重、該找誰。

圖 1:同一顆測試掛掉,壞命名與好命名帶給你的資訊量差距
這件事有個很實際的判準:
測試報告上只看得到名稱的時候,你能不能判斷要不要現在處理?
能,名稱就合格了。
二、命名公式:功能:情境 → 預期結果
業界的命名慣例流派不少,但骨架都一樣:名稱裡要同時看得到「情境」跟「預期結果」。

圖 2:命名公式的拆解,以及業界常見的三種寫法
三種主流寫法,挑一種就好
對端對端測試,我偏好最後一種:測試報告會直接被 PM、客服看到,中文完整句省掉一層翻譯。單元測試因為數量大、跑得快,用三段式的英文命名比較省事。這件事全隊統一比選哪一種重要。
把功能層級放進 describe
每個測試名稱前面都掛「登入:」會很囉唆。業界普遍的做法是用 describe 分組,報告會自動把兩層串起來:
Playwright 範例
test.describe('登入', () => {
test('輸入正確帳密 → 進入首頁', async ({ page }) => { /* ... */ });
test('輸入錯誤密碼 → 顯示錯誤訊息且停留在登入頁', async ({ page }) => {
await loginPage.goto();
await loginPage.login('user@example.com', '錯的密碼');
await expect(page.getByRole('alert')).toHaveText('帳號或密碼錯誤');
await expect(page).toHaveURL(/\/login/);
});
test('連續錯 5 次 → 帳號被鎖定 15 分鐘', async ({ page }) => { /* ... */ });
});
報告上會顯示成「登入 › 輸入錯誤密碼 → 顯示錯誤訊息且停留在登入頁」,效果一樣,但程式碼裡不用重複寫。
常見的命名壞味道
讓 AI 每次產出都符合慣例
AI 產出的名稱常偏籠統,它不知道哪些資訊對你們重要。把命名慣例交代下去,以後每次產出自動符合:
你可以這樣對 Claude Code 說:
以後幫我寫測試,名稱一律用「功能:情境 → 預期結果」格式,用繁體中文,功能層級放在 test.describe。請把這條慣例寫進 CLAUDE.md,連同前幾篇建立的其他慣例(語意化定位優先、測試獨立、資料自建自清)一起整理進去。
三、CLAUDE.md:給 AI 的團隊規範
上面那段指令引出一個好東西。CLAUDE.md 是放在專案裡的一個文字檔,Claude Code 每次工作都會先讀它。你們累積的所有慣例寫在這裡,等於幫 AI 上了團隊的入職訓練。
它對人也一樣好用:新同事來了,這份文件就是「我們的測試怎麼寫」的說明書。一份文件,人跟 AI 共用,維護一次就好。第 5 篇存下來的指令範本,很多可以搬進來升級成慣例。
CLAUDE.md 範例
# 測試撰寫慣例
## 命名
- 格式:功能:情境 → 預期結果,用繁體中文
- 功能層級用 test.describe 分組,test 標題只寫「情境 → 預期結果」
## 定位
- 優先序:getByRole > getByLabel > getByText > getByTestId
- 禁止用 CSS class、XPath、nth-child 定位
## 測試資料
- 每個測試自建自清,測試之間不共用帳號
- 建資料走 API,不走 UI
## 等待
- 只用自動等待與 expect 輪詢,禁止 waitForTimeout
## 標籤
- @smoke:每個功能挑一個最具代表性的正向流程
- @critical:登入、下單、付款
- @slow:單次執行超過 30 秒
## 註解
- 只寫「為什麼」,不寫「做什麼」
寫得具體一點。「測試要寫好」這種句子 AI 讀了也不知道要幹嘛,「禁止 waitForTimeout」它就照做。
四、組織:按業務功能分
目錄結構的唯一標準:新同事想找「訂單相關的測試」,三秒內找不找得到?
圖 3:一個找得到東西的目錄結構,以及三種常見的歧途
主結構:資料夾對應業務功能
按業務功能分(auth、orders、payment)是業界最耐用的做法,理由很單純:需求是按功能來的。PM 說「訂單取消的規則改了」,你直接進 orders/ 就對了。
常見的歧途有三種:
• 按人分(tests/david/、tests/mary/):人一走,那批測試就變孤兒,沒人敢動。
• 按時間分(tests/2025-Q3/、tests/sprint12/):半年後沒人知道裡面是什麼,只會愈積愈多。
• 按測試類型分(tests/smoke/、tests/regression/):看起來最合理,實際上訂單的測試會散在三個資料夾。而且同一個測試常常同時是冒煙又是回歸,你要放哪個?類型這件事,交給標籤處理更乾淨。
配套的支援資料夾
除了業務功能資料夾,業界的專案通常還有這幾個固定成員:
檔案命名也順手統一:kebab-case、一個檔案對應一個功能、副檔名 .spec.ts。單一檔案超過三百行就是拆分的訊號,通常代表這個「功能」其實是兩個功能。
檔案內部:三段式的測試主體
測試內部的組織,業界慣例叫 AAA(Arrange-Act-Assert),中文講就是「準備、執行、驗證」三段。空一行分開就有效果:
三段式範例
test('庫存足夠 → 成功建立訂單', async ({ page, api }) => {
// 準備
const user = await api.createUser();
const sku = await api.createProduct({ stock: 10 });
// 執行
await orderPage.goto(sku);
await orderPage.addToCart(1);
await orderPage.checkout();
// 驗證
await expect(page.getByTestId('order-status')).toHaveText('已成立');
});
一個測試只驗證一件事。當你發現自己在寫第二段「執行」,通常代表這裡該拆成兩個測試了。
五、標籤:同一批測試,多種切法
Playwright 支援給測試上標籤,例如 @smoke(冒煙)、@critical(核心流程)、@slow(慢速)。執行時可以只跑某個標籤。

圖 4:同一批測試,靠標籤切出三種執行集合
於是「五分鐘冒煙集」「發版前核心集」「每晚完整集」是同一批測試的三種切面,不用維護三套。
上標籤與執行
test('庫存足夠 → 成功建立訂單', { tag: ['@smoke', '@critical'] }, async ({ page }) => {
// ...
});
// 執行時挑選:
// npx playwright test --grep @smoke
// npx playwright test --grep @critical
// npx playwright test --grep-invert @slow
業界常見的標籤大致分三類,不用一次全上,先有 @smoke 跟 @critical 就很夠用:
哪些進冒煙、哪些算核心,是風險判斷,這件事你來標,AI 來執行:
你可以這樣對 Claude Code 說:
請幫這批測試上標籤:登入、下單、付款流程標 @critical;每個功能挑一個最具代表性的正向測試標 @smoke。然後告訴我怎麼只執行 @smoke 的測試。
六、註解:寫為什麼,不寫做什麼
「做什麼」不用寫,程式本身(尤其 Page Object 化之後)已經會說話,寫了反而變成重複維護——改了程式忘了改註解,註解就開始騙人。
不用寫的註解
// 這行點擊登入按鈕 ← 沒有資訊,程式碼自己就看得懂
await loginPage.submit();
值得寫的是「為什麼」:為什麼這裡要多等一個訊號?為什麼這個場景用固定帳號?這些決策脈絡程式看不出來,偏偏是半年後的維護者最需要的:
值得寫的註解
// 訂單建立 API 有已知的非同步延遲(平均 300ms),
// 立刻查列表會抓不到。已回報 ORD-1421,修好後可以拿掉這個 timeout。
await expect(page.getByTestId('order-row')).toBeVisible({ timeout: 10000 });
// 用固定帳號 qa-locked@example.com:
// 建帳號的 API 不支援直接建立「已鎖定」狀態,只能借用這個預埋帳號。
const user = FIXED_USERS.locked;
請 Claude Code 補註解時,把這個原則講清楚,不然它會很勤奮地幫每一行寫「這行在點擊按鈕」。
你可以這樣對 Claude Code 說:
幫這個檔案補註解,但只寫「為什麼」:為什麼要特別等待、為什麼用固定資料、為什麼這個斷言選這個條件。任何從程式碼就看得出來的操作說明都不要寫。