E2E 测试
单元测试验证函数逻辑,E2E 测试验证用户流程——从打开浏览器到完成一次完整操作。Playwright 是目前最强的 E2E 测试框架:跨浏览器、自动等待、网络拦截、视觉对比,且与 Next.js 深度集成。
1. Playwright 为什么适合 Next.js
1.1 与 Cypress 的对比
| 维度 | Playwright | Cypress |
|---|---|---|
| 浏览器支持 | 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),不需要手动 sleep 或 waitForTimeout。
// 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:测试构建产物而非开发服务器,更接近生产环境 - 失败时上传 artifact:
playwright-report/和test-results/(含 trace),方便远程调试 - 重试策略:
retries: 2减少 flaky test 对 CI 的干扰
6. 调试工具箱
| 工具 | 命令 | 用途 |
|---|---|---|
| UI 模式 | pnpm e2e --ui | 可视化运行,实时看浏览器操作 |
| Debug 模式 | pnpm e2e --debug | 逐步执行,断点调试 |
| Trace Viewer | npx playwright show-trace trace.zip | 时间旅行回放:截图 + DOM + 网络 + console |
| Codegen | npx 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 覆盖所有场景