E2E 测试的价值
端到端(E2E)测试模拟真实用户操作,验证整个应用流程是否正常工作。它是测试金字塔的顶层,虽然数量少但覆盖关键路径。
| 测试类型 | 速度 | 覆盖范围 | 维护成本 |
|---|---|---|---|
| 单元测试 | 毫秒级 | 单个函数 | 低 |
| 集成测试 | 秒级 | 模块交互 | 中 |
| E2E 测试 | 分钟级 | 完整流程 | 高 |
主流框架对比
核心特性对比
| 特性 | Playwright | Cypress |
|---|---|---|
| 浏览器支持 | Chrome, Firefox, Safari, Edge | Chrome, Firefox, Edge |
| 多标签页 | ✅ 原生支持 | ❌ 不支持 |
| 跨域 | ✅ 无限制 | ⚠️ 需要配置 |
| 并行执行 | ✅ 内置 | ✅ 需要 Dashboard |
| 网络拦截 | ✅ 强大 | ✅ 强大 |
| 移动端模拟 | ✅ 完整 | ⚠️ 有限 |
| 调试体验 | 好 | 优秀 |
Playwright 优势
// 多浏览器测试配置
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
projects: [
{ name: 'Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'Safari', use: { ...devices['Desktop Safari'] } },
{ name: 'Mobile', use: { ...devices['iPhone 14'] } }
],
// 并行执行
workers: process.env.CI ? 4 : undefined
})
Playwright 的多标签页支持是独特优势:
test('新标签页登录流程', async ({ context }) => {
const page1 = await context.newPage()
await page1.goto('/login')
// 点击第三方登录,在新标签页打开
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page1.click('button.oauth-login')
])
// 在新标签页完成授权
await newPage.fill('#username', 'user')
await newPage.click('#authorize')
// 回到原页面验证登录成功
await page1.waitForSelector('.user-profile')
})
Cypress 优势
Cypress 的交互式调试体验是其最大亮点:
// cypress/e2e/login.cy.js
describe('登录流程', () => {
beforeEach(() => {
cy.visit('/login')
})
it('正确的用户名密码应成功登录', () => {
cy.get('[data-testid="username"]').type('admin')
cy.get('[data-testid="password"]').type('password123')
cy.get('button[type="submit"]').click()
// 自动等待和重试
cy.url().should('include', '/dashboard')
cy.get('.welcome-message').should('contain', '欢迎回来')
})
})
关键说明: Cypress 的命令会自动等待元素出现并自动重试,减少了显式等待代码。而 Playwright 需要更明确地处理等待逻辑。
实际场景对比
表单测试
// Playwright
test('表单提交', async ({ page }) => {
await page.goto('/contact')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('textarea[name="message"]', '这是一条测试消息')
await page.click('button[type="submit"]')
await expect(page.locator('.success')).toBeVisible()
})
// Cypress
it('表单提交', () => {
cy.visit('/contact')
cy.get('input[name="email"]').type('test@example.com')
cy.get('textarea[name="message"]').type('这是一条测试消息')
cy.get('button[type="submit"]').click()
cy.get('.success').should('be.visible')
})
API Mock
// Playwright - 使用 route 拦截
test('显示 mock 数据', async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: '模拟用户' }])
})
})
await page.goto('/users')
await expect(page.locator('.user-name')).toHaveText('模拟用户')
})
// Cypress - 使用 intercept
it('显示 mock 数据', () => {
cy.intercept('GET', '/api/users', {
body: [{ id: 1, name: '模拟用户' }]
}).as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
cy.get('.user-name').should('have.text', '模拟用户')
})
文件上传
// Playwright
test('上传头像', async ({ page }) => {
await page.goto('/profile')
await page.setInputFiles('input[type="file"]', 'test-files/avatar.jpg')
await expect(page.locator('.avatar-preview')).toBeVisible()
})
// Cypress
it('上传头像', () => {
cy.visit('/profile')
cy.get('input[type="file"]').selectFile('cypress/fixtures/avatar.jpg')
cy.get('.avatar-preview').should('be.visible')
})
选型建议
选择 Playwright 的场景
- 需要测试多浏览器(特别是 Safari)
- 涉及多标签页、多窗口操作
- 需要跨域测试
- 已有 TypeScript 技术栈
- 需要强大的并行能力
选择 Cypress 的场景
- 团队刚接触 E2E 测试
- 重视调试体验和开发者友好性
- 项目以 Chrome 用户为主
- 喜欢直观的 GUI 测试运行器
性能对比
在相同测试场景下:
| 指标 | Playwright | Cypress |
|---|---|---|
| 启动时间 | 较快 | 较慢 |
| 单测试执行 | 相近 | 相近 |
| 并行测试 | 优秀 | 需要付费 |
| CI 集成 | 简单 | 简单 |
迁移成本
如果从一个框架迁移到另一个:
// Cypress -> Playwright 的主要变化
// cy.visit() -> page.goto()
// cy.get() -> page.locator()
// cy.click() -> .click()
// .should('be.visible') -> await expect().toBeVisible()
// cy.intercept() -> page.route()
两个框架的 API 设计理念不同:Cypress 是链式调用,Playwright 是 async/await。迁移需要一定工作量,但核心测试逻辑可以复用。
最佳实践
- 从关键路径开始:优先覆盖登录、支付等核心流程
- 保持测试独立:每个测试应该能独立运行
- 使用 Page Object 模式:封装页面操作,提高可维护性
- 合理使用 Mock:隔离外部依赖,提高稳定性
- 定期维护:随着应用变化及时更新测试
无论选择哪个框架,坚持编写和维护 E2E 测试都将显著提升产品质量。


