E2E 测试

单元测试验证函数逻辑,E2E 测试验证用户流程——从打开浏览器到完成一次完整操作。Playwright 是目前最强的 E2E 测试框架:跨浏览器、自动等待、网络拦截、视觉对比,且与 Next.js 深度集成。

1. Playwright 为什么适合 Next.js

1.1 与 Cypress 的对比

维度PlaywrightCypress
浏览器支持Chromium + Firefox + WebKit(真正的跨浏览器)主要 Chrome,Firefox/WebKit 实验性
多标签/多窗口✅ 原生支持❌ 不支持
网络拦截page.route() 功能完善cy.intercept()
并行执行原生多 worker 并行需付费 Dashboard
调试体验Trace Viewer(时间旅行回放)Time Travel(仅 DOM 快照)
移动端模拟内置设备 profiles(Pixel、iPhone 等)只能手动设置 viewport

核心优势:Playwright 用真实浏览器引擎运行测试,而非嵌入式浏览器。WebKit 覆盖意味着你能在 CI 中测到 Safari 的兼容性问题——这是 Cypress 做不到的。

1.2 安装与配置

pnpm add -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,            // CI 中禁止 .only
  retries: process.env.CI ? 2 : 0,          // CI 中失败重试
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',                // 失败时录制 trace
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile', use: { ...devices['iPhone 13'] } },
  ],
  webServer: {                               // 自动启动 Next.js
    command: process.env.CI ? 'pnpm start' : 'pnpm dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

关键配置项解读

  • fullyParallel:测试文件间并行,显著减少总耗时
  • trace: 'on-first-retry':只在失败重试时录制 trace,节省资源
  • webServer:Playwright 会自动启动并等待你的 Next.js 应用就绪,CI 中用 pnpm start(跑构建产物),开发时用 pnpm dev

2. 编写 E2E 测试

2.1 核心模式

Playwright 的测试模式很统一:导航 → 操作 → 断言。所有断言自带自动等待(auto-retry),不需要手动 sleepwaitForTimeout

// e2e/auth.spec.ts — 登录流程
import { test, expect } from '@playwright/test'
 
test('login with valid credentials', async ({ page }) => {
  await page.goto('/login')                                    // 导航
  await page.getByLabel('邮箱').fill('[email protected]')      // 操作
  await page.getByLabel('密码').fill('password123')
  await page.getByRole('button', { name: '登录' }).click()
  await expect(page).toHaveURL('/dashboard')                   // 断言(自动等待 URL 变化)
  await expect(page.getByText('Welcome, Alice')).toBeVisible()
})

2.2 元素定位策略

和 RTL 一样,Playwright 也推荐语义化定位

方法使用场景示例
getByRole按钮、链接、标题page.getByRole('button', { name: '提交' })
getByLabel表单输入框page.getByLabel('邮箱')
getByText文本内容page.getByText('发布成功')
getByTestId兜底page.getByTestId('sidebar')

2.3 认证状态复用

避免每个测试都走一遍登录流程——用 storageState 保存并复用认证状态:

// e2e/auth.setup.ts — 预登录,保存 Cookie
import { test as setup, expect } from '@playwright/test'
 
setup('authenticate', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('邮箱').fill('[email protected]')
  await page.getByLabel('密码').fill('password123')
  await page.getByRole('button', { name: '登录' }).click()
  await expect(page).toHaveURL('/dashboard')
  await page.context().storageState({ path: 'e2e/.auth/user.json' })
})

playwright.config.ts 中声明依赖关系,其他测试自动复用登录态:

projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: 'e2e/.auth/user.json' },
    dependencies: ['setup'],
  },
]

3. 网络拦截

page.route() 可以拦截并 Mock 任何网络请求——用于测试加载状态、错误处理、空数据等边界场景:

test('handles API error gracefully', async ({ page }) => {
  await page.route('/api/products', (route) => route.fulfill({ status: 500 }))
  await page.goto('/products')
  await expect(page.getByText('加载失败')).toBeVisible()
})

等待特定请求完成:

const [response] = await Promise.all([
  page.waitForResponse('/api/contact'),
  page.getByRole('button', { name: '发送' }).click(),
])
expect(response.status()).toBe(200)

4. 视觉回归测试

Playwright 内置截图对比——第一次运行生成基准图,之后每次运行自动对比像素差异:

test('homepage visual regression', async ({ page }) => {
  await page.goto('/')
  await page.waitForLoadState('networkidle')
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.01,  // 允许 1% 像素差异(抗字体渲染抖动)
  })
})
pnpm e2e --update-snapshots  # 首次运行 / 更新基准图

实践建议

  • 视觉回归测试只在 CI 的固定环境中运行——不同操作系统的字体渲染不同,本地跑会频繁误报
  • maxDiffPixelRatio: 0.01 容忍微小的渲染差异
  • 组件级别做截图对比(expect(element).toHaveScreenshot())比全页截图更稳定

5. CI 集成

5.1 GitHub Actions 配置

# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
 
jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'pnpm' }
      - run: pnpm install
      - run: npx playwright install --with-deps chromium
      - run: pnpm build
      - run: pnpm e2e --project=chromium
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: playwright-report, path: playwright-report/ }

5.2 CI 优化要点

  • 只跑 Chromium:CI 中跑三个浏览器太慢,Chromium 覆盖 90% 的场景。跨浏览器测试放在每周定时 CI 或 release 前
  • pnpm start:测试构建产物而非开发服务器,更接近生产环境
  • 失败时上传 artifactplaywright-report/test-results/(含 trace),方便远程调试
  • 重试策略retries: 2 减少 flaky test 对 CI 的干扰

6. 调试工具箱

工具命令用途
UI 模式pnpm e2e --ui可视化运行,实时看浏览器操作
Debug 模式pnpm e2e --debug逐步执行,断点调试
Trace Viewernpx playwright show-trace trace.zip时间旅行回放:截图 + DOM + 网络 + console
Codegennpx playwright codegen localhost:3000录制操作自动生成测试代码

Trace Viewer 是 Playwright 的杀手级功能:每一步操作的截图、DOM 快照、网络请求、控制台日志全部记录,可以像调试器一样来回跳转。CI 中失败的测试,下载 trace 文件打开就能看到"当时浏览器里发生了什么"。

7. 测试策略

7.1 该测与不该测

✅ 该测❌ 不该测
核心用户流程(注册、登录、下单)纯 UI 样式(用视觉回归)
关键页面渲染(首页、仪表盘)工具函数逻辑(用单元测试)
错误处理(404、网络错误)第三方服务(Mock 掉)
跨页面状态流转每个组件的细节交互

7.2 数量建议

项目规模E2E 测试数量额外
小项目5-10核心流程
中型项目20-50+ 视觉回归
大型项目50-100+ 视觉回归 + 性能

7.3 避免 Flaky Test

Flaky test(时过时不过的测试)是 E2E 测试的最大痛点:

  • 永远不用 waitForTimeout——用 expect 自动等待或 waitForResponse
  • 测试间完全隔离——每个测试不依赖其他测试的副作用
  • Mock 不稳定的外部服务——第三方 API、时间相关逻辑用 page.clock
  • CI 中用 retries: 2——网络抖动等偶发问题自动重试

本章小结

  • Playwright vs Cypress:Playwright 跨浏览器更强、并行更好、Trace Viewer 调试体验碾压
  • 测试模式:导航 → 操作 → 断言,所有断言自动等待
  • 认证复用storageState 保存登录态,避免每个测试重复登录
  • 网络拦截page.route() Mock API,测试各种边界状态
  • 视觉回归toHaveScreenshot() 像素对比,只在 CI 固定环境运行
  • CI 集成:只跑 Chromium 节省时间,失败上传 trace,重试减少 flaky
  • 核心心法:E2E 只覆盖核心用户流程,不要试图用 E2E 覆盖所有场景