文章

E2E测试深度解析

端到端测试:用真实浏览器模拟真实用户操作完整业务流程,Playwright 与 Cypress 的核心用法与选型。

E2E测试深度解析

一句话概括

E2E(端到端)测试用真实浏览器模拟真实用户,从打开页面、点击、输入到跳转,走完一条完整业务链路(比如”登录 → 加购 → 下单 → 支付”),验证整个系统前后端协同是否正常。它是测试金字塔的塔尖——数量最少、最接近用户、也最慢最脆弱,但抓的是单元测试和组件测试都抓不到的集成问题。两大主流工具是 Playwright(微软,多浏览器 + 自动等待 + 并行强)和 Cypress(开发者体验好,但多标签/多浏览器是弱项)。

核心知识点

1. 用 Playwright 模拟真实用户流程

Playwright 的核心体验是自动等待——它默认会等元素可交互再操作,你几乎不需要手写 sleep:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { test, expect } from '@playwright/test';

test('登录后能看到用户名', async ({ page }) => {
  await page.goto('https://example.com/login');

  // 按角色/占位符定位,自动等待元素出现并可交互
  await page.getByPlaceholder('用户名').fill('zhangsan');
  await page.getByPlaceholder('密码').fill('secret123');
  await page.getByRole('button', { name: '登录' }).click();

  // 断言跳转后的结果,expect 也会自动重试等待
  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByText('张三,欢迎回来')).toBeVisible();
});

与单元测试的关键区别:它跑的是真实浏览器 + 真实后端 + 真实网络,不是 mock 出来的环境。因此一条 E2E 能同时验证前端、后端、数据库、网关是否协同正常。

2. 自动等待 vs 手动等待

这是 Playwright 和 Cypress 最大的设计差异,也是 E2E 测试稳定性的分水岭:

1
2
3
4
5
6
7
8
9
10
// ❌ 手动 sleep:固定等待,慢机器超时、快机器浪费,是 flaky 的根源
await page.waitForTimeout(3000);

// ✅ 自动等待:等到"具体条件成立"为止
await page.getByText('加载完成').waitFor();        // 等元素出现
await expect(page.getByText('数据已加载')).toBeVisible(); // 等可见
await page.waitForResponse(res => res.url().includes('/api/users')); // 等响应

// Cypress 同款:cy.get().should() 内置重试
cy.get('[data-cy=result]').should('contain', '成功');

自动等待的哲学是”断言即等待“——描述你期望的最终状态,框架帮你重试直到满足或超时。任何手写的 sleep 都是坏味道。

3. 多浏览器与移动端模拟

Playwright 的杀手锏是一份脚本跑遍所有浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// playwright.config.js
export default defineConfig({
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',  use: { ...devices['Desktop Safari'] } },
    // 移动端
    { name: 'mobile',  use: { ...devices['iPhone 13'] } },
  ],
});

// 单测内临时模拟视口/设备
test.use({ viewport: { width: 375, height: 667 } }); // 模拟手机宽度
await page.emulateMedia({ colorScheme: 'dark' });    // 模拟暗色模式

这对验证跨浏览器兼容性、响应式布局、移动端 H5 尤其有价值——同样的流程在 Safari 上崩了,单元测试完全测不出来。

4. 拦截网络与数据准备

E2E 的稳定性和速度,靠的是控制网络,而不是依赖真实接口的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 拦截 API 返回固定数据,让测试不依赖后端真实数据
await page.route('**/api/users', route => {
  route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([{ id: 1, name: '张三' }])
  });
});

// 或继续请求但改响应
await page.route('**/api/config', async route => {
  const resp = await route.fetch();
  const json = await resp.json();
  json.featureFlag = true; // 打开某个开关
  await route.fulfill({ response: resp, json });
});

拦截网络能让你测”接口报错时页面怎么展示”、”数据为空时的空状态”这些平时很难触发的边界场景。

5. Cypress vs Playwright 怎么选

维度CypressPlaywright
浏览器支持Chrome 系 + Firefox(多标签弱)Chrome/Firefox/Safari 全支持
自动等待内置重试(should)内置自动等待,更强
并行/性能需 Dashboard 付费原生多 worker 并行,免费
跨标签/多页面受限原生支持
调试体验交互式 runner + 时间旅行trace viewer + 录像

简言之:看重调试体验、团队已用 Cypress → 继续 Cypress;需要跨浏览器、多标签、免费并行 → Playwright。2026 年的趋势是 Playwright 在社区和采用率上持续占优。

其实你每天都在用

  • 电商下单链路:登录→搜索→加购→结算→支付,这条链路一旦断裂(比如库存接口挂了导致结算按钮失效),只有 E2E 能整条链路抓到
  • 发版前的冒烟测试:每次上线前自动跑一遍”首页能打开、能登录、核心功能能点”,这就是 E2E 冒烟在守门
  • CI 里的 playwright.yml:GitHub Actions 流水线里那个”运行浏览器测试”的 step,跑的就是 E2E,红了就阻断合入
  • 线上巡检/拨测:定时用脚本模拟用户访问首页、下单,监控”服务是不是还活着”,本质是生产环境的 E2E
  • 演示回放排查问题:测试失败时 Playwright 的 trace/trace viewer 能回放每一步操作和截图,帮你秒定位”哪一步崩了”

常见误解(FAQ)

❌ 误区一:”E2E 测试越多越好,覆盖所有场景”

E2E 慢、贵、脆(一个网络抖动就红),数量一多 CI 跑半小时、天天 flaky。正确策略是测试金字塔:大量单元测试打底、适量组件测试、少量 E2E 覆盖核心链路。E2E 测的是”用户最关键的那几条路走得通”,不是穷举所有分支。

❌ 误区二:”E2E 测试慢是正常的,多加点 sleep 保证稳定”

sleep 恰恰是 flaky 的制造者而不是解决者。固定等待在慢机器上超时、在快机器上浪费,还掩盖了竞态。正确做法是自动等待(断言即等待),让测试”等到条件成立”而不是”睡够时间”。测试不稳定时,先怀疑 sleep 和未拦截的网络,而不是盲目加延时。

❌ 误区三:”E2E 测试连接真实后端数据最真实”

连真实后端意味着测试依赖数据状态——今天有数据明天空了、账号被别人改密码了,测试就红。正确做法是拦截 API(mock 网络层)或使用独立的测试环境 + 种子数据,让每次测试从确定的初始状态出发。E2E 要测的是”流程逻辑对不对”,不是”生产数据此刻是什么样”。

❌ 误区四:”选择器用 CSS class 和 id 最方便定位”

class 和 id 是给样式和 DOM 用的,会随重构频繁变化,而且用户根本感知不到。应该优先用 getByRole、getByText、getByLabel 这些用户视角的定位器,data-testid 只作最后手段。用角色定位还能顺带暴露无障碍(a11y)问题——找不到 role="button" 往往说明那个 div 按钮语义残缺。

一句话总结

E2E 测试是”用真实浏览器替用户把最关键的几条路走一遍“——自动等待代替 sleep、网络拦截代替真实数据、少量精挑代替大量堆砌,才能在抓集成问题的同时,不被慢和 flaky 拖垮。

本文由作者按照 CC BY 4.0 进行授权