E2E 测试
E2E(End-to-End)测试从用户视角验证完整功能流程——打开浏览器、导航到页面、点击按钮、填写表单、检查结果。它是测试金字塔的顶层,数量最少但信心最强。Nuxt4 通过
@nuxt/test-utils集成 Playwright,提供了一套开箱即用的 E2E 测试方案。本章从 E2E 测试的价值定位讲起,深入 Playwright 与 Nuxt 的集成原理,再到视觉回归和 CI 最佳实践。
1. E2E 测试的价值与边界
1.1 E2E 测试解决什么问题
单元测试验证"每块砖是好的",E2E 测试验证"整栋楼能住人"。有些 bug 单元测试永远抓不到:
- 组件间通信错误:父组件传了
videoId,子组件期望id——两个组件的单测都过了,但组合起来就挂了 - 路由和中间件联动:未登录用户访问
/dashboard应该重定向到/login,这需要路由守卫、Session 状态、页面组件协同工作 - SSR Hydration 不匹配:服务端渲染的 HTML 和客户端激活后的 DOM 不一致,只在真实浏览器中才能发现
- 第三方集成:支付回调、OAuth 登录、视频播放器——这些涉及真实的浏览器环境和网络请求
1.2 E2E 测试的代价
| 维度 | 代价 | 应对 |
|---|---|---|
| 速度 | 每个测试 2-10 秒 | 只测关键流程,不测细节 |
| 稳定性 | 网络抖动、动画时序、DOM 变化 | 合理等待策略,避免 sleep |
| 维护 | UI 改动频繁破坏测试 | 用 data-testid 而非 CSS 选择器 |
| 调试 | 失败时不容易定位原因 | 截图 + trace + 视频录制 |
核心原则:E2E 测试应该少而精。测 10 个核心用户旅程(注册→登录→上传→播放→退出),不要试图用 E2E 覆盖所有边界条件——那是单元测试的职责。
1.3 什么适合 E2E 测试
| 适合 | 不适合 |
|---|---|
| 用户注册/登录流程 | 输入校验的每种错误消息 |
| 核心业务流程(上传→发布→播放) | 组件的 props 组合 |
| 支付/订阅流程 | 工具函数的边界条件 |
| 权限隔离验证(普通用户不能访问 admin) | 样式和布局细节 |
| 搜索和筛选的完整流程 | API 响应的数据结构 |
2. Playwright + Nuxt4 集成
2.1 为什么是 Playwright
Playwright 是 Microsoft 开发的浏览器自动化框架,与 Nuxt4 / Vitest 生态深度集成。相比 Cypress:
| 特性 | Playwright | Cypress |
|---|---|---|
| 浏览器支持 | Chromium + Firefox + WebKit | 仅 Chromium(免费版) |
| 多标签页 / 多窗口 | ✅ 原生支持 | ❌ 不支持 |
| 并行执行 | ✅ 进程级并行 | 需付费 |
| 网络拦截 | ✅ 精细控制 | ✅ |
| 执行速度 | 快(无进程内通信开销) | 较慢(运行在浏览器内) |
| 学习曲线 | 中等 | 低(可视化调试好) |
2.2 @nuxt/test-utils 的 E2E 模式
@nuxt/test-utils 的 E2E 模式做了一件关键的事:自动启动 Nuxt 开发服务器。你不需要手动启动服务器再跑测试,setup() 会在测试开始前构建并启动 Nuxt 应用,测试结束后自动关闭。
import { setup, createPage, url } from '@nuxt/test-utils/e2e'
import { describe, it, expect } from 'vitest'
describe('Homepage', async () => {
await setup({
server: true,
browser: true,
browserOptions: {
type: 'chromium',
},
})
it('renders the hero section', async () => {
const page = await createPage('/')
const title = await page.textContent('h1')
expect(title).toContain('AI 视频平台')
})
})createPage(path) 是 @nuxt/test-utils 对 Playwright page 的封装,自动拼接 Nuxt 服务器的 URL。返回的 page 对象就是标准的 Playwright Page,可以使用所有 Playwright API。
2.3 url() 辅助函数
url(path) 返回 Nuxt 测试服务器的完整 URL。当你需要直接使用 Playwright 原生 API 时:
import { url } from '@nuxt/test-utils/e2e'
it('navigates correctly', async () => {
const page = await createPage()
await page.goto(url('/videos'))
await expect(page).toHaveURL(/\/videos/)
})3. 核心用户流程测试
3.1 页面导航和 SSR 验证
it('SSR renders video list', async () => {
const page = await createPage('/videos')
// 验证 SSR:页面加载时内容已经在 HTML 中
const html = await page.content()
expect(html).toContain('视频列表')
// 验证 Hydration:客户端 JS 激活后交互正常
await page.click('[data-testid="sort-by-date"]')
await page.waitForSelector('[data-testid="video-card"]')
})3.2 表单交互和验证
it('validates login form', async () => {
const page = await createPage('/login')
// 空表单提交
await page.click('[data-testid="login-btn"]')
await expect(page.locator('[data-testid="email-error"]')).toBeVisible()
// 填写正确信息
await page.fill('[data-testid="email-input"]', '[email protected]')
await page.fill('[data-testid="password-input"]', 'Password123')
await page.click('[data-testid="login-btn"]')
// 登录成功后跳转
await page.waitForURL('**/dashboard')
})3.3 认证流程测试
E2E 测试认证流程时,通常不想每个测试都走一遍登录。Playwright 的 storageState 可以保存认证状态:
// tests/e2e/auth.setup.ts — 全局 setup
import { setup, createPage, url } from '@nuxt/test-utils/e2e'
await setup({ server: true, browser: true })
const page = await createPage('/login')
await page.fill('[data-testid="email-input"]', '[email protected]')
await page.fill('[data-testid="password-input"]', 'TestPassword123')
await page.click('[data-testid="login-btn"]')
await page.waitForURL('**/dashboard')
// 保存认证状态到文件
await page.context().storageState({ path: '.auth/admin.json' })后续测试直接使用保存的认证状态,跳过登录步骤。
3.4 API Mock 与网络拦截
E2E 测试中,某些外部依赖(支付 API、AI 模型服务)不应该真正调用。Playwright 的 route API 可以拦截和 mock 网络请求:
it('handles AI generation failure gracefully', async () => {
const page = await createPage('/create')
// 拦截 AI 生成 API,返回错误
await page.route('**/api/ai/generate', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'AI 服务暂时不可用' }),
})
})
await page.click('[data-testid="generate-btn"]')
await expect(page.locator('[data-testid="error-toast"]')).toContainText('AI 服务暂时不可用')
})这比真正调用 AI 服务快得多,也更稳定——不受外部服务可用性的影响。
4. 视觉回归测试
4.1 什么是视觉回归
视觉回归测试通过截图对比检测 UI 变化。每次运行测试时截图,与基线截图逐像素比较。如果差异超过阈值,测试失败。
它解决了一个棘手问题:CSS 改动的副作用很难用断言捕获。你改了一个全局 padding,可能影响了 20 个页面的布局——单元测试和功能 E2E 测试都发现不了。
4.2 Playwright 截图断言
it('video card matches snapshot', async () => {
const page = await createPage('/videos')
// 等待内容稳定
await page.waitForSelector('[data-testid="video-card"]')
await page.waitForLoadState('networkidle')
// 截图对比
await expect(page).toHaveScreenshot('video-list.png', {
maxDiffPixelRatio: 0.01, // 允许 1% 的像素差异
})
})首次运行会生成基线截图(存在 tests/e2e/__snapshots__/ 目录中),后续运行与基线对比。如果 UI 有意改动了,用 --update-snapshots 更新基线。
4.3 视觉测试的实践要点
- 稳定性:动态内容(时间戳、随机推荐)会导致截图每次不同。用 mock 数据或
page.evaluate()冻结动态内容。 - 字体渲染:不同操作系统的字体渲染有细微差异。CI 中使用 Docker 容器(
mcr.microsoft.com/playwright)确保一致性。 - 响应式:同一页面在不同视口宽度下布局不同。用
page.setViewportSize()测试关键断点(mobile 375px、tablet 768px、desktop 1440px)。 - 暗色模式:如果支持主题切换,需要分别截图测试。
5. E2E 测试架构
5.1 Page Object 模式
当测试数量增长后,直接操作 page.click('[data-testid="xxx"]') 会导致大量重复。Page Object 模式将页面交互封装为类:
// tests/e2e/pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto(url('/login'))
}
async login(email: string, password: string) {
await this.page.fill('[data-testid="email-input"]', email)
await this.page.fill('[data-testid="password-input"]', password)
await this.page.click('[data-testid="login-btn"]')
}
async expectError(message: string) {
await expect(this.page.locator('[data-testid="error-message"]'))
.toContainText(message)
}
}it('shows error for wrong password', async () => {
const page = await createPage()
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('[email protected]', 'wrong-password')
await loginPage.expectError('邮箱或密码错误')
})Page Object 的好处:UI 结构变化时只需修改 Page Object 类,不需要改每个测试。
5.2 测试数据管理
E2E 测试需要已知的、可预测的数据。三种策略:
| 策略 | 方式 | 适用场景 |
|---|---|---|
| Seed 数据库 | 测试前用种子脚本写入数据,测试后清理 | 测试 CRUD 流程 |
| API Mock | Playwright route 拦截,返回固定数据 | 不依赖数据库的 UI 测试 |
| 创建即测试 | 测试中先创建数据,再验证 | 完整流程验证 |
推荐组合:Seed + API Mock。核心流程用 Seed 数据保证真实性,外部依赖用 Mock 保证稳定性。
6. CI 中的 E2E 测试
6.1 GitHub Actions 配置要点
# .github/workflows/e2e.yml
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npx vitest run tests/e2e
env:
CI: true
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
test-results/
playwright-report/关键细节:
- 只安装需要的浏览器(
chromium),不安装全部三个,节省 CI 时间 - 失败时上传截图和 trace 文件,方便远程调试
- 用专门的测试数据库,不影响其他环境
6.2 调试失败的 E2E 测试
Playwright 的 Trace Viewer 是杀手级调试工具。在配置中开启 trace:
browserOptions: {
type: 'chromium',
launch: { trace: 'on-first-retry' },
}失败时生成 trace.zip,用 npx playwright show-trace trace.zip 打开——可以看到测试的每一步操作、截图、DOM 快照、网络请求、控制台日志。就像录像回放一样还原失败现场。
6.3 并行执行与隔离
Playwright 默认按文件级别并行执行测试。每个测试文件使用独立的浏览器上下文(context),Cookie 和 localStorage 互不干扰。
如果测试间有数据库写入冲突(两个测试同时创建同名用户),需要:
- 每个测试使用唯一的测试数据(比如邮箱加上随机后缀)
- 或者串行执行有数据冲突的测试文件(
test.describe.configure({ mode: 'serial' }))
7. E2E 测试 Checklist
测试覆盖
- 用户注册 → 登录 → 退出完整流程
- 核心业务流程(上传 → 编辑 → 发布 → 播放)
- 权限隔离(普通用户不能访问 admin 页面)
- 关键错误处理(API 失败、网络超时)
- SSR 首屏渲染验证
稳定性
- 使用
data-testid选择器,不依赖 CSS 类名 - 使用 Playwright 的 auto-wait,不用
sleep - 外部 API 用
route拦截 mock - 动态数据冻结或 mock
CI
- 失败时上传截图和 trace
- 测试数据库隔离
- 视觉回归使用 Docker 容器保证一致性
本章小结
- E2E 定位:少而精,只测核心用户旅程和组件间协作,不替代单元测试
- Playwright + @nuxt/test-utils:
setup()自动启动 Nuxt 服务器,createPage()创建浏览器页面 - 网络拦截:
page.route()mock 外部 API,提升速度和稳定性 - 视觉回归:截图对比检测 CSS 副作用,注意字体渲染和动态内容的一致性
- Page Object:封装页面交互,UI 变化时只改一处
- CI 调试:Trace Viewer 回放失败现场,trace + 截图是远程调试的关键
- 稳定性铁律:
data-testid选择器 + auto-wait + API mock + 数据隔离