傳說「易筋經」是一套以「以動入靜、伸展筋骨、強身健體、宣通氣血」為宗旨的養生之道。在自動化測試裡,我們也需要一套類似的內功心法,來確保服務的品質。今天,我們會透過以測試金字塔為指引,用 AI 協助撰寫貫通服務的測試案例。這就像是修煉「易筋經」,從最基本的筋骨(單元測試)、逐步打通經脈(整合測試)、最後整個身體(E2E測試)協同運作,達到至高境界。
「測試金字塔」原則是由 Martin Fowler 在2012 提出。這個原則的核心概念強調測試案例應該合理分布在不同層級,才達到最好的測試策略。透過這樣的分布,我們可以確保自動化測試在不同層面都能夠功能是否正確或 API 和服務正常、使用者流程是否順暢。但隨著產品的特性與專案不同,我們會將測試金字塔分為不同的層級:單元測試、元件/服務整合測試、契約測試(Contract Testing)、整合測試、端點到端點測試 (End-to-End Tests)。
在原本的「測試金字塔原則」裡,單元測試會是程式碼中的最小單位,例如:函式或者類別,並且不會依賴於其他元件,例如:資料庫、需要檔案存取。但這樣實踐會造成單元測試案例數量很多,程式碼的邏輯功能正常,但行為不一定正常。如果產品都是以 API 串接,單元測試的單元定義延伸至「API 行為」為最小單位,例如:保證 API 在正常使用下,回傳 200。或是其他異常下,會丟出 Excetion 或 500 等行為。
包含元件/服務整合測試、契約測試(Contract Testing)、整合測試,首先,元件或服務測試,就是測試兩個元件或服務測試是否能夠正常溝通協調,例如:購物車系統、付費系統、訂單系統,當使用者按下付費後,能夠正確被引導到付費系統,並且付費成功或失敗,都會在通知訂單系統,這流程可能會涉及多個 API 的溝通。在呼叫 API 前,也會檢查 Schema 是否正確等等。在應用層測試主要是確保跨服務或是跨 API 的功能正常,並不會要求使完整的工作路徑,換句換說,我可以建立假購物資料、並且假的第三方付費系統,確保服務間的資料流與邏輯正常和異常都能夠如預期。
這層會包含驗收測試(Acceptance Testing)和模擬使用者從頭到尾的完整操作(UI Testing),確保他們的路徑正常且能夠順利完成。雖然在「測試金字塔原則」裡,E2E 測試的數量應該最少,因為它的執行時間最長、維護成本也最高。但這類測試卻是使用者才會遇到的案例,如果你的 E2E 測試能夠涵蓋使用者所有核心路徑,那基本上可以確保他們不會遇到不該發生的問題,使用者體驗自然會更好。
因此在一開始的時候,在專案初期,如果你的單元測試或整合測試還不夠完善,先投入資源撰寫足夠的 E2E 測試,這就像先用最外層的防護罩,保護使用者會遇到的所有路徑,即便代價是執行時間較長,且問題發生時,需要花更多時間去定位。這也是為什麼非開發人員(例如產品經理或業務)最關心的就是這個環節。他們在意的不是程式碼細節,而是「使用者能不能順利完成任務?」。隨著對系統越來越瞭解,並逐步提升其他測試的涵蓋度後,就可以將一部分 E2E 測試的責任,慢慢往下移到應用層測試和單元測試,最終形成一個完美的測試金字塔。這不僅能大幅縮短執行時間,也能在問題發生時,更快地找到根源。
我們將以一個建立訂單的 RESTful API 為例,探討如何從三個不同層次來編寫測試,並善用 Playwright 的強大功能來完成。
想像我們有一個訂單 API,其運作方式如下:
POST /v1/orders
:使用 JSON 格式來發送請求並且建立訂單我們的測試目標將涵蓋:
我們會先從需求面撰寫測試情境,接著使用 Playwright 來模擬使用者建立訂單是否正常,接著驗證跨 API 到 API 間是否能夠交互溝通,最後才是檢查我們的建立訂單 API 功能是否正常。
在動手寫程式碼之前,我們可以先用 Gherkin 語法寫下測試情境,讓所有相關人員都能清楚理解測試意圖。
tests/features/order-api.feature
Feature: API 契約驗證 - 建立訂單服務
身為第三方服務,我希望透過訂單 API 建立訂單,並確保回應內容符合預期行為。
Scenario: 呼叫 /v1/orders 並建立訂單成功
Given 當訂單服務的 API 正常運行
When 以產品 "iPhone 17" 和數量 1 發送 POST 請求
Then 訂單回應該狀態碼應該是 "201 Created"
And 回應內容應該包含訂單編號、產品名稱、數量、使用者編號、建立時間
tests/steps/order-api-steps.ts
import { expect } from '@playwright/test';
import { createBdd } from 'playwright-bdd';
import { z } from 'zod';
// 定義訂單回應的資料結構,包含訂單編號、產品名稱、數量、訂購者編號與訂單建立時間
const OrderSchema = z.object({
id: z.string().nonempty(),
productName: z.string().nonempty(),
quantity: z.number().int().positive(),
userId: z.string().nonempty(),
createdAt: z.string().datetime().or(z.string().nonempty()),
});
type OrderResponse = z.infer<typeof OrderSchema>;
// 定義一個共享的 context,用於在不同步驟間傳送資料
const { Given, When, Then } = createBdd<{ apiResponse: any }>();
// 前置條件
Given('當訂單服務的 API 正常運行', async () => {
// 此步驟用於檢查訂單服務的 health 是正常的,並且能夠呼叫 API
});
// 執行動作
When('以產品 {string} 和數量 {int} 發送 POST 請求', async ({ api, apiResponse }, productName: string, quantity: number) => {
// 使用 api.post 發送請求,並將回應儲存在共享變數中
apiResponse.response = await api.post('/api/v1/orders', {
data: { productName, quantity }
});
});
// 確認結果
Then('回應狀態碼應為 {int} Created', async ({ apiResponse }, status: number) => {
expect(apiResponse.response.status(), 'API 狀態碼應該是 201 Created').toBe(status);
});
Then('回應內容應該包含訂單編號、產品名稱、數量、使用者編號、建立時間 ', async ({ apiResponse }) => {
const json = await apiResponse.response.json();
const parsed = OrderSchema.safeParse(json);
// 驗證資料是否符合我們定義的 Schema
if (!parsed.success) {
console.error(parsed.error.format());
}
expect(parsed.success, '回應內容應該包含訂單對應的資訊').toBe(true);
// (可選)更進一步驗證某個欄位
const order = parsed.data;
expect(order.productName).toBe('iPhone 17');
expect(order.quantity).toBe(1);
});
這個測試只使用了 playwright.request 來呼叫 API。它沒有開啟任何瀏覽器,而是直接驗證 API 的外部行為,和資料是否被正確地寫入資料庫。這就是整合測試的目的:專注於元件與服務之間的互動。通常這種整合測試,因為不需要 UI 或啟動瀏覽器,也可以直接使用原本的單元測試框架(例如:Jest)實作。
tests/api/order-integration.spec.ts
import { test, expect, APIRequestContext } from '@playwright/test';
import { z } from 'zod';
import { findOrderInDatabase, clearDatabase } from '../../src/utils/db-client';
const OrderSchema = z.object({
id: z.string().nonempty(),
productName: z.string().nonempty(),
quantity: z.number().int().positive(),
userId: z.string().nonempty(),
createdAt: z.string().datetime().or(z.string().nonempty()),
});
let api: APIRequestContext;
test.beforeAll(async ({ playwright }) => {
api = await playwright.request.newContext({
baseURL: process.env.API_BASE_URL ?? 'https://your-api.example.com/api/v1',
extraHTTPHeaders: { 'Content-Type': 'application/json' },
});
});
test.afterAll(async () => {
await api.dispose();
await clearDatabase();
});
test('建立訂單成功:應回傳 201 且資料庫有記錄', async () => {
const payload = { productName: 'Laptop', quantity: 1 };
// ACT: 直接呼叫 API
const res = await api.post('/orders', { data: payload });
// ASSERT 1: 驗證 API 回應
expect(res.status(), '應為 201 Created').toBe(201);
const json = await res.json() as unknown;
const parsed = OrderSchema.safeParse(json);
expect(parsed.success, '回應應符合 Order 合約').toBe(true);
// ASSERT 2: 驗證後端依賴 (資料庫)
const orderInDb = await findOrderInDatabase(parsed.success ? parsed.data.id : '');
expect(orderInDb).toBeTruthy();
expect(orderInDb?.productName).toBe(payload.productName);
});
這個測試完全從使用者的視角出發。我們只用 page.fill 和 page.click 這些瀏覽器方法,來模擬完整的使用者旅程。程式碼中沒有任何直接的 API 呼叫,但我們知道這些 UI 操作會觸發後端的 API。這就是E2E 測試:驗證從前端到後端的完整流程。
tests/api/order-notification.spec.ts
import { test, expect } from '@playwright/test';
test('UI 建立訂單後應顯示成功提示與結果', async ({ page }) => {
// 前置:登入流程
await page.goto('https://your-app.example.com/login');
await page.getByTestId('login-username').fill('user01');
await page.getByTestId('login-password').fill('pass123');
await page.getByTestId('login-submit').click();
// 執行:填寫訂單並提交
await page.goto('https://your-app.example.com/orders/new');
await page.getByTestId('product-name').fill('Laptop');
await page.getByTestId('quantity').fill('1');
await page.getByTestId('submit-order').click();
// 驗證:確認 UI 顯示與列表更新
await expect(page.getByText('訂單建立成功')).toBeVisible();
await expect(page.getByRole('row', { name: /Laptop/ })).toBeVisible();
});
根據上面所學習到內功知識,我們可以撰寫對應的 .prompt 我們在需要撰寫 BDD 測試步驟 (Steps) 來協助完成。這份 prompt 適用於你想要將 Gherkin 語法轉換為可執行程式碼時。它會指示 AI 根據你當前開啟的 .feature 檔案,生成對應的 steps 實作。
.github/prompts/generate-bdd-steps.prompt
根據當前打開的 Gherkin Feature File,撰寫對應的 Playwright-BDD 測試步驟(steps.ts)。
請確保程式碼使用 TypeScript,並遵循 AAA 架構。
這份 prompt 適用於你想要直接測試 API,並驗證其與後端依賴(例如:資料庫)的互動時。它會指示 AI 撰寫一個不依賴瀏覽器的純 API 測試。
.github/prompts/api-integration-test.prompt
撰寫一個針對 RESTful API 的整合測試。
請使用 Playwright 的 `request` fixture,並撰寫對應的測試在 tests/api/ 資料夾。
我會提供 URI 和回應格式,如果沒有,可以詢問我或是確認程式碼是否有對應的 API
原則:
1. 驗證正確的狀態碼 201
2. 驗證正確的回應內容
3. 必要時驗證是否正確寫入資料庫
撰寫一個模擬使用者完整旅程的 E2E 測試。
我會提供一個使用 Gherkin 的 .feature 檔案,請使用 Playwright 的 `page` 物件,模擬使用者的流程。
原則:
1. 使用者能順利完成流程,如果流程不清楚,請詢問我。
2. 對於每個流程需要驗證並且檢查是否符合條件。
3. 程式碼可以撰寫在 `tests/e2e/` 資料夾。
在專案中,功能釋出的時間往往不可能將所有測試都測試完成才交付給客戶。要如何選擇測試案例,其實是測試人員需要把關的一部分,通常會根據風險高或是重要功能當作優先選擇的對象,但如果選擇的測試案例不好,則會讓關鍵功能的問題上到生產環境才知道,如果測試案例太多,則會花費許多的測試時間。
今天我們學習以測試金字塔為核心,從最底層的 API 測試開始,逐步向上建立完整的測試體系。這不僅能確保單一功能的正確性、服務的協調運作,更能保證整個服務的穩定性與品質。我們也瞭解,在現代的測試江湖中,AI 是我們最強大的輔助。透過撰寫 Gherkin 測試情境與 prompt,我們將「內功心法」傳授給 AI,讓它協助我們完成繁重的測試撰寫工作。但別忘記,AI 雖然能替代撰寫程式,但無法思考測試案例的架構和背後邏輯關係。