Playwright E2E 端對端測試完整教學:Next.js 專案從設定到 CI/CD 自動化
你的 Next.js 專案有寫測試嗎?如果有,大概是 Jest 搭配 React Testing Library 做元件測試。但你有沒有想過,那些「使用者從登入到完成購買」的完整流程,該怎麼自動化測試?這就是端對端測試(E2E Testing)的領域,而 Playwright 是目前最好的選擇。
Playwright 與 Cypress 的比較
先回答一個最常見的問題:為什麼選 Playwright 而不是 Cypress?
| 比較項目 | Playwright | Cypress |
|---|---|---|
| 維護者 | Microsoft | Cypress.io |
| 支援瀏覽器 | Chromium, Firefox, WebKit | Chrome, Firefox, Edge |
| 多分頁 / 多視窗 | 原生支援 | 不支援 |
| 平行測試 | 原生支援 | 需付費 Dashboard |
| iFrame 支援 | 完整支援 | 有限 |
| API 測試 | 內建 request context | 需外掛 |
| 自動等待 | 智慧等待 | 自動重試 |
| 速度 | 很快(無瀏覽器啟動延遲) | 較慢 |
Playwright 最大的優勢是多瀏覽器支援和原生平行執行。在 CI 環境中,這代表更短的測試時間和更廣的覆蓋範圍。
在 Next.js 專案中設定 Playwright
安裝非常簡單:
npm init playwright@latest
# 或者手動安裝
npm install -D @playwright/test
npx playwright install
初始化後會產生 playwright.config.ts,讓我們來調整它:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
重點設定說明:fullyParallel 讓所有測試平行執行;webServer 設定讓 Playwright 自動啟動 Next.js dev server;trace: 'on-first-retry' 在重試時錄製完整的操作軌跡,除錯超方便。如果你的 Next.js 專案有用到use cache 快取策略,記得在測試環境中適當處理快取行為。
撰寫第一個測試
先從簡單的頁面測試開始:
// e2e/home.spec.ts
import { test, expect } from '@playwright/test';
test.describe('首頁測試', () => {
test('應該正確載入首頁', async ({ page }) => {
await page.goto('/');
// 驗證頁面標題
await expect(page).toHaveTitle(/我的部落格/);
// 驗證導覽列存在
const nav = page.getByRole('navigation');
await expect(nav).toBeVisible();
// 驗證文章列表有內容
const articles = page.getByRole('article');
await expect(articles).toHaveCount(10);
});
test('應該能搜尋文章', async ({ page }) => {
await page.goto('/');
// 使用搜尋功能
await page.getByPlaceholder('搜尋文章...').fill('React');
await page.getByRole('button', { name: '搜尋' }).click();
// 驗證結果
await expect(page.getByText('搜尋結果')).toBeVisible();
const results = page.getByTestId('search-result');
await expect(results.first()).toBeVisible();
});
});
Playwright 的 Locator API 是它最棒的設計之一。getByRole、getByText、getByPlaceholder 這些方法讓你用跟使用者一樣的角度找到元素,測試可讀性極高。
Page Object Model 設計模式
當測試變多後,你會發現很多操作是重複的(像是登入流程)。Page Object Model(POM)是解決這個問題的標準做法:
// e2e/pages/login.page.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('密碼');
this.submitButton = page.getByRole('button', { name: '登入' });
this.errorMessage = page.getByTestId('error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
// 在測試中使用
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';
test('使用者應該能登入', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password123');
await expect(page).toHaveURL('/dashboard');
});
Mock API 與測試隔離
E2E 測試的一大挑戰是如何處理外部 API。Playwright 內建了強大的網路攔截功能:
test('應該顯示使用者資料', async ({ page }) => {
// Mock API 回應
await page.route('**/api/user/profile', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
name: '測試用戶',
email: '[email protected]',
avatar: '/default-avatar.png'
}),
});
});
await page.goto('/profile');
await expect(page.getByText('測試用戶')).toBeVisible();
});
// 模擬 API 錯誤
test('API 錯誤時應顯示錯誤提示', async ({ page }) => {
await page.route('**/api/user/profile', (route) =>
route.fulfill({ status: 500 })
);
await page.goto('/profile');
await expect(page.getByText('載入失敗')).toBeVisible();
});
這讓你的測試不依賴真實的後端服務,跑起來更快也更穩定。
視覺回歸測試
Playwright 內建截圖比對功能,這對注重視覺品質的專案非常重要,特別是當你的頁面使用了View Transitions 頁面轉場動畫時:
test('首頁視覺快照', async ({ page }) => {
await page.goto('/');
// 等待所有圖片載入
await page.waitForLoadState('networkidle');
// 全頁截圖比對
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.01,
fullPage: true,
});
});
test('元件視覺快照', async ({ page }) => {
await page.goto('/components');
const card = page.getByTestId('feature-card');
await expect(card).toHaveScreenshot('feature-card.png');
});
第一次執行會自動建立基準截圖,之後每次執行都會跟基準比對。如果有差異會產生差異圖片,一目了然。
GitHub Actions CI/CD 整合
最後把測試整合進 CI/CD:
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Build Next.js
run: npm run build
- name: Run E2E tests
run: npx playwright test
env:
CI: true
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
這跟你設定React Server Components 效能最佳化的流程可以整合在一起,確保每次部署前都通過完整的 E2E 測試。
認證流程測試技巧
認證是 E2E 測試最常見的挑戰。Playwright 提供了 storageState 來重複使用登入狀態:
// e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'e2e/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('密碼').fill('password123');
await page.getByRole('button', { name: '登入' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});
然後在 config 中設定 setup project,所有需要認證的測試都會自動載入登入狀態,不需要每個測試都跑一次登入流程。
總結與最佳實踐
Playwright 讓 E2E 測試變得前所未有地簡單和可靠。以下是我的建議:從最重要的使用者流程開始寫測試(不要試圖一開始就覆蓋所有東西);善用 Page Object Model 管理可重用邏輯;在 CI 中固定瀏覽器版本避免不穩定;善用 trace 功能快速除錯失敗的測試。E2E 測試不是要取代單元測試和整合測試,而是在測試金字塔的頂端補上最後一塊拼圖。
你可能也喜歡
探索其他領域的精選好文