一、什么是端到端界面测试
端到端界面测试,通常简称 E2E 测试,是站在真实用户视角验证应用是否能够完整工作的测试方式。它不是只测试一个函数、一个组件或一个接口,而是让浏览器像用户一样打开页面、点击按钮、输入内容、跳转路由、等待网络请求、检查界面结果。
前端 E2E 测试关注的是:用户能不能完成一条真实业务路径。
例如:
- 用户能否登录成功。
- 用户能否搜索商品并进入详情页。
- 用户能否填写表单并提交。
- 用户能否完成下单、支付、取消、退款等关键流程。
- 管理后台用户能否筛选数据、编辑记录、导出文件。
E2E 测试的价值不在于覆盖所有代码分支,而在于覆盖最关键的用户路径。它是一种偏业务验收的测试。
二、端到端测试和其他测试的区别
前端常见测试可以分为单元测试、组件测试、集成测试和端到端测试。它们不是互相替代关系,而是解决不同层级的问题。
| 测试类型 | 测试对象 | 运行环境 | 主要目标 | 常用工具 |
|---|---|---|---|---|
| 单元测试 | 函数、工具方法、状态逻辑 | Node.js 或模拟环境 | 验证局部逻辑正确 | Jest、Vitest |
| 组件测试 | 单个 UI 组件 | jsdom 或真实浏览器 | 验证组件渲染与交互 | Testing Library、Cypress Component、Playwright CT |
| 集成测试 | 多模块协作 | 模拟或真实环境 | 验证模块之间能否配合 | Jest、Vitest、MSW |
| E2E 测试 | 完整用户流程 | 真实浏览器 | 验证用户路径是否可用 | Playwright、Cypress、WebdriverIO |
测试金字塔强调:底层测试数量多、速度快;顶层 E2E 测试数量少、价值高、成本也高。
E2E 测试不适合替代所有测试。如果把所有细节都放进 E2E,测试会变慢、变脆、维护成本很高。更合理的做法是:基础逻辑用单元测试保证,组件交互用组件测试保证,核心链路用 E2E 兜底。
三、为什么前端需要端到端界面测试
现代前端应用越来越复杂。一个用户操作背后可能涉及路由、状态管理、接口请求、缓存、权限、表单校验、埋点、弹窗、文件上传、WebSocket、第三方 SDK 等多个环节。
单元测试可以证明局部函数正确,但不能证明用户真的能完成操作。E2E 测试补上的正是这部分信心。
E2E 测试尤其适合保护以下场景:
- 登录、注册、找回密码。
- 首页首屏、导航、搜索。
- 下单、支付、订单确认。
- 表单创建、编辑、删除。
- 权限控制和路由守卫。
- 多步骤向导流程。
- 数据导入、导出、上传、下载。
- 跨页面状态流转。
- SSR 或水合后的页面可用性。
- 移动端 H5 在不同视口下的关键交互。
四、E2E 测试的基本执行过程
一条 E2E 测试通常包含准备环境、打开页面、执行操作、等待结果、断言界面、清理数据几个阶段。
一个典型测试用例可以这样描述:
- 打开登录页面。
- 输入用户名和密码。
- 点击登录按钮。
- 等待跳转到首页。
- 断言页面出现用户昵称。
- 断言 URL 已经进入首页路径。
五、主流 E2E 测试工具
1. Playwright
Playwright 是当前非常常用的现代 E2E 测试框架,支持 Chromium、Firefox、WebKit,支持多标签页、自动等待、网络拦截、截图、视频、Trace Viewer、移动设备模拟等能力。
它的优势是:
- 跨浏览器能力强。
- 自动等待机制优秀。
- 支持多上下文和多用户场景。
- CI 体验较好。
- 调试工具完整。
- 对现代前端应用支持好。
2. Cypress
Cypress 也是非常流行的前端测试工具,具有良好的交互式调试体验。它适合开发阶段快速编写和调试测试,也支持组件测试。
它的优势是:
- 开发体验直观。
- 命令链写法清晰。
- 时间旅行调试体验好。
- 社区生态成熟。
3. WebdriverIO 和 Selenium
Selenium 是较早期的浏览器自动化方案,WebdriverIO 在 WebDriver 生态上提供了更现代的封装。它们在大型企业、多语言、多浏览器兼容测试中仍然常见。
4. Puppeteer
Puppeteer 是 Chrome 团队推出的浏览器自动化工具,常用于自动化脚本、截图、PDF、爬取、性能分析,也可以写 E2E 测试。但如果重点是完整测试框架,Playwright 通常更省心。
六、以 Playwright 为例搭建 E2E 测试
1. 安装依赖
bash
npm init playwright@latest
如果已有项目,也可以手动安装:
bash
npm install -D @playwright/test
npx playwright install
2. 基础目录结构
text
project
├── package.json
├── playwright.config.ts
├── tests
│ ├── login.spec.ts
│ ├── search.spec.ts
│ └── order.spec.ts
└── src
3. 配置文件示例
ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30 * 1000,
expect: {
timeout: 5000
},
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [
['html'],
['list']
],
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
webServer: {
command: 'npm run dev -- --host 127.0.0.1',
url: 'http://127.0.0.1:5173',
reuseExistingServer: !process.env.CI
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] }
}
]
});
4. 第一个测试用例
ts
import { test, expect } from '@playwright/test';
test('用户可以登录并进入首页', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('用户名').fill('demo');
await page.getByLabel('密码').fill('123456');
await page.getByRole('button', { name: '登录' }).click();
await expect(page).toHaveURL(/\/home/);
await expect(page.getByText('欢迎回来')).toBeVisible();
});
这个用例表达的是用户行为,而不是实现细节。好的 E2E 测试应该尽量像真实用户操作页面一样书写。
七、定位元素的最佳实践
E2E 测试最容易变脆的地方是元素选择器。如果使用 .container > div:nth-child(2) > button 这种选择器,一旦页面结构微调,测试就会失败。
更推荐的选择器优先级是:
- 用户可感知语义:
getByRole、getByLabel、getByText。 - 可访问性属性:
aria-label、aria-labelledby。 - 稳定测试属性:
data-testid。 - CSS 选择器:只在没有更好选择时使用。
ts
await page.getByRole('button', { name: '提交' }).click();
await page.getByLabel('邮箱').fill('user@example.com');
await page.getByPlaceholder('请输入关键词').fill('手机');
await page.getByTestId('order-submit').click();
八、等待机制和异步稳定性
E2E 测试不是写脚本点得越快越好,而是要等页面进入正确状态。现代框架通常有自动等待能力,但仍然要避免不稳定写法。
不推荐:
ts
await page.click('.submit');
await page.waitForTimeout(3000);
await expect(page.locator('.success')).toBeVisible();
推荐:
ts
await page.getByRole('button', { name: '提交' }).click();
await expect(page.getByText('提交成功')).toBeVisible();
如果要等待接口响应:
ts
const responsePromise = page.waitForResponse(response => {
return response.url().includes('/api/orders') && response.status() === 200;
});
await page.getByRole('button', { name: '提交订单' }).click();
await responsePromise;
await expect(page.getByText('订单提交成功')).toBeVisible();
九、测试真实后端还是 Mock 接口
E2E 测试有两种常见方式:连接真实后端,或者在测试中拦截网络请求并返回 Mock 数据。
连接真实后端更接近真实场景,但成本较高,容易受环境和数据影响。Mock 接口更稳定、更快,但不能验证前后端真实集成。
实际项目中通常会混合使用:
- 核心链路使用测试环境真实后端。
- 边界状态和异常状态使用 Mock。
- 第三方支付、短信、风控等外部服务使用模拟层。
- 难以构造的数据场景使用接口拦截。
Playwright 网络拦截示例:
ts
await page.route('**/api/user/profile', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
name: '测试用户',
role: 'admin'
})
});
});
await page.goto('/profile');
await expect(page.getByText('测试用户')).toBeVisible();
十、测试数据管理
E2E 测试必须认真处理测试数据。没有数据治理的 E2E 测试会逐渐变成随机失败的来源。
常见策略包括:
- 每个用例创建自己的数据。
- 使用固定测试账号,但避免共享可变状态。
- 每次测试后清理数据。
- 使用独立测试租户或测试空间。
- 用 API 准备数据,而不是通过 UI 一步步创建所有前置状态。
- 数据名称加入唯一标识,避免并发冲突。
ts
import { test, expect } from '@playwright/test';
async function createArticle(request, title: string) {
const response = await request.post('/api/articles', {
data: { title, content: 'E2E 测试内容' }
});
return response.json();
}
test('用户可以查看文章详情', async ({ page, request }) => {
const title = `文章-${Date.now()}`;
const article = await createArticle(request, title);
await page.goto(`/articles/${article.id}`);
await expect(page.getByRole('heading', { name: title })).toBeVisible();
});
十一、登录态处理
登录是 E2E 测试最常见的前置条件。如果每个用例都通过 UI 输入账号密码,会导致测试很慢,也会增加失败概率。
更推荐的方式是:
- 单独测试登录流程本身。
- 其他需要登录态的用例复用已保存的登录状态。
- 使用 API 登录生成 Cookie 或 Token。
- 使用独立测试账号,避免多人共享污染。
Playwright 保存登录态示例:
ts
import { test as setup, expect } from '@playwright/test';
setup('登录并保存状态', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('用户名').fill('demo');
await page.getByLabel('密码').fill('123456');
await page.getByRole('button', { name: '登录' }).click();
await expect(page).toHaveURL(/\/home/);
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
使用登录态:
ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
storageState: 'playwright/.auth/user.json'
}
});
登录态流程:
十二、Page Object 模式
当测试用例越来越多时,如果每个文件都直接写大量选择器和操作步骤,维护会变困难。Page Object 可以把页面操作封装起来,让测试用例只表达业务意图。
ts
import { expect, Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.page.getByLabel('用户名').fill(username);
await this.page.getByLabel('密码').fill(password);
await this.page.getByRole('button', { name: '登录' }).click();
}
async expectLoginSuccess() {
await expect(this.page).toHaveURL(/\/home/);
await expect(this.page.getByText('欢迎回来')).toBeVisible();
}
}
测试用例:
ts
import { test } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('用户可以登录', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('demo', '123456');
await loginPage.expectLoginSuccess();
});
Page Object 不是越厚越好。它适合封装稳定页面的常用动作,不适合把所有断言和流程都塞成黑盒。测试代码仍然应该能读出业务场景。
十三、断言应该关注用户可见结果
E2E 测试的断言应该尽量关注用户看得见、感知得到的结果,而不是内部实现。
推荐断言:
ts
await expect(page.getByText('保存成功')).toBeVisible();
await expect(page.getByRole('button', { name: '提交' })).toBeDisabled();
await expect(page).toHaveURL(/\/orders\/\d+/);
await expect(page.getByRole('heading', { name: '订单详情' })).toBeVisible();
不推荐过度断言:
ts
await expect(page.locator('.modal > div:nth-child(2)')).toHaveClass(/active/);
await expect(page.locator('#app')).toHaveAttribute('data-v-app');
好的断言应该回答:用户是否真的得到了正确反馈。
十四、常见业务场景测试示例
1. 搜索流程
ts
import { test, expect } from '@playwright/test';
test('用户可以搜索商品', async ({ page }) => {
await page.goto('/');
await page.getByPlaceholder('搜索商品').fill('手机');
await page.getByRole('button', { name: '搜索' }).click();
await expect(page).toHaveURL(/keyword=%E6%89%8B%E6%9C%BA/);
await expect(page.getByText('搜索结果')).toBeVisible();
await expect(page.getByText('手机')).toBeVisible();
});
2. 表单提交流程
ts
import { test, expect } from '@playwright/test';
test('用户可以创建项目', async ({ page }) => {
await page.goto('/projects/new');
await page.getByLabel('项目名称').fill('E2E 测试项目');
await page.getByLabel('项目描述').fill('用于验证端到端流程');
await page.getByRole('button', { name: '创建' }).click();
await expect(page.getByText('创建成功')).toBeVisible();
await expect(page.getByRole('heading', { name: 'E2E 测试项目' })).toBeVisible();
});
3. 权限控制流程
ts
import { test, expect } from '@playwright/test';
test('普通用户不能访问管理后台', async ({ page }) => {
await page.goto('/admin');
await expect(page.getByText('无访问权限')).toBeVisible();
await expect(page.getByRole('link', { name: '返回首页' })).toBeVisible();
});
4. 文件上传流程
ts
import { test, expect } from '@playwright/test';
import path from 'path';
test('用户可以上传头像', async ({ page }) => {
await page.goto('/settings/profile');
await page.getByLabel('上传头像').setInputFiles(path.resolve(__dirname, 'fixtures/avatar.png'));
await page.getByRole('button', { name: '保存' }).click();
await expect(page.getByText('保存成功')).toBeVisible();
await expect(page.getByAltText('用户头像')).toBeVisible();
});
5. 多标签页和新窗口
ts
import { test, expect } from '@playwright/test';
test('用户可以打开帮助页面', async ({ page, context }) => {
await page.goto('/');
const pagePromise = context.waitForEvent('page');
await page.getByRole('link', { name: '帮助中心' }).click();
const helpPage = await pagePromise;
await expect(helpPage.getByRole('heading', { name: '帮助中心' })).toBeVisible();
});
十五、移动端和响应式界面测试
前端界面不只运行在桌面浏览器,也可能运行在手机浏览器、App WebView、平板、折叠屏等环境。E2E 测试可以通过不同视口验证关键交互是否可用。
Playwright 配置移动设备:
ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'desktop-chrome',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'iphone-13',
use: { ...devices['iPhone 13'] }
}
]
});
响应式测试重点:
- 移动端导航是否可打开和关闭。
- 表单输入是否被键盘遮挡。
- 弹窗是否超出屏幕。
- 固定底部按钮是否可点击。
- 横向滚动是否异常。
- 图片、卡片、列表是否布局错乱。
十六、视觉回归测试
E2E 测试通常验证功能是否正确,视觉回归测试则关注页面是否发生了非预期的视觉变化。它通过截图对比发现样式、布局、颜色、间距、字体、组件状态的变化。
Playwright 支持截图断言:
ts
import { test, expect } from '@playwright/test';
test('首页视觉回归', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('home-page.png');
});
为了减少截图误报,应注意:
- 固定测试数据。
- 禁用动画或等待动画结束。
- 屏蔽时间、随机数、广告等动态区域。
- 使用固定字体和固定视口。
- 不把视觉回归当成所有 UI 变化的审批机制。
十七、可访问性测试
E2E 测试也可以结合可访问性检查,发现按钮无名称、表单无 label、颜色对比不足、页面结构不合理等问题。
常用工具是 axe-core。
bash
npm install -D @axe-core/playwright
示例:
ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('首页没有严重可访问性问题', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
可访问性测试不是为了取代人工体验检查,而是把常见问题自动化拦截在提交阶段。
十八、性能相关 E2E 检查
E2E 测试可以做轻量性能检查,例如首屏是否出现、关键接口是否太慢、交互后是否长时间无反馈。但不建议把复杂性能基准全部塞进普通 E2E 用例,否则容易波动。
示例:
ts
import { test, expect } from '@playwright/test';
test('首页关键内容在合理时间内可见', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: '首页' })).toBeVisible({ timeout: 3000 });
});
采集 Performance Timing:
ts
const timing = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.startTime,
load: navigation.loadEventEnd - navigation.startTime
};
});
console.log(timing);
十九、调试 E2E 测试
E2E 测试失败时,不能只看终端里的一行错误。更有效的调试方式是结合截图、视频、Trace、网络请求和控制台日志。
Playwright 常用调试命令:
bash
npx playwright test --debug
npx playwright test --headed
npx playwright test tests/login.spec.ts --trace on
npx playwright show-report
Trace Viewer 可以看到:
- 每一步操作。
- DOM 快照。
- 控制台输出。
- 网络请求。
- 截图变化。
- 断言失败位置。
常见失败原因:
- 元素定位不稳定。
- 测试数据被污染。
- 接口返回慢或环境不稳定。
- 页面动画尚未结束。
- 登录态失效。
- 并发执行时账号或数据互相影响。
- 断言过于依赖实现细节。
二十、CI 中运行 E2E 测试
E2E 测试最终应该进入 CI 流程。它可以在合并前、每日定时、发布前、部署后执行。
典型 CI 流程:
GitHub Actions 示例:
yaml
name: e2e
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
CI 中需要特别注意:
- 使用稳定的测试环境。
- 控制并发数量。
- 保存失败现场。
- 对不稳定用例进行治理,而不是无限重试。
- 区分阻塞发布的主链路测试和非阻塞的巡检测试。
二十一、E2E 测试用例应该怎么选
E2E 测试贵,所以不能什么都测。选择用例时可以按业务价值和风险排序。
优先覆盖:
- 用户访问量最高的路径。
- 直接影响收入或核心转化的路径。
- 历史上经常出问题的路径。
- 多系统协作的路径。
- 手工回归成本很高的路径。
- 线上故障影响很大的路径。
不建议大量覆盖:
- 纯展示文案。
- 低价值边角功能。
- 很容易用单元测试覆盖的纯逻辑。
- 内部实现细节。
- 频繁变化且非核心的视觉布局。
二十二、如何降低 E2E 测试维护成本
E2E 测试维护成本高,核心原因是它依赖完整环境、真实页面和业务数据。降低成本的关键不是少写,而是写得稳定。
实践建议:
- 测试用户行为,不测试 DOM 实现细节。
- 使用稳定选择器。
- 避免固定等待时间。
- 用 API 准备数据。
- 每个用例独立,不依赖执行顺序。
- 控制用例数量,优先主链路。
- 对 flaky 测试建立治理机制。
- 把公共动作抽成清晰的 helper 或 Page Object。
- CI 失败时保留截图、视频和 Trace。
- 定期删除已经没有价值的测试。
二十三、常见反模式
1. 用 E2E 覆盖所有细节
这会导致测试慢、反馈慢、维护困难。细粒度逻辑应该交给单元测试和组件测试。
2. 大量使用固定等待
ts
await page.waitForTimeout(5000);
这类写法既慢又不稳定。页面快时浪费时间,页面慢时仍然失败。
3. 依赖测试执行顺序
text
先运行创建订单测试,再运行取消订单测试
一旦并发执行或单独运行,就会失败。每个用例都应该能独立运行。
4. 使用真实生产环境写入数据
生产环境 E2E 可以做只读巡检,但不应该随意创建、修改、删除真实业务数据。
5. 只靠重试掩盖问题
重试可以缓解偶发环境波动,但不能解决测试本身不稳定。长期 flaky 的用例会让团队失去对测试结果的信任。
二十四、Vue 项目中的 E2E 测试
Vue 项目通常可以用 Playwright 或 Cypress 做 E2E。无论使用 Vue Router、Pinia、Vite、Nuxt,测试重点都应该放在用户路径上。
Vue + Vite 示例配置:
json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
Vue 页面示例:
vue
<template>
<form @submit.prevent="submit">
<label>
用户名
<input v-model="username" />
</label>
<button type="submit">保存</button>
<p v-if="success">保存成功</p>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const username = ref('');
const success = ref(false);
function submit() {
success.value = true;
}
</script>
E2E 测试:
ts
import { test, expect } from '@playwright/test';
test('Vue 表单可以保存', async ({ page }) => {
await page.goto('/profile');
await page.getByLabel('用户名').fill('张三');
await page.getByRole('button', { name: '保存' }).click();
await expect(page.getByText('保存成功')).toBeVisible();
});
二十五、React 项目中的 E2E 测试
React 项目同样适合用 Playwright 或 Cypress。对于 React Router、Redux、Zustand、Next.js 等应用,E2E 测试应该关注完整用户流程。
React 页面示例:
tsx
import { useState } from 'react';
export function ProfileForm() {
const [username, setUsername] = useState('');
const [success, setSuccess] = useState(false);
return (
<form onSubmit={(event) => {
event.preventDefault();
setSuccess(true);
}}>
<label>
用户名
<input value={username} onChange={(event) => setUsername(event.target.value)} />
</label>
<button type="submit">保存</button>
{success ? <p>保存成功</p> : null}
</form>
);
}
E2E 测试:
ts
import { test, expect } from '@playwright/test';
test('React 表单可以保存', async ({ page }) => {
await page.goto('/profile');
await page.getByLabel('用户名').fill('李四');
await page.getByRole('button', { name: '保存' }).click();
await expect(page.getByText('保存成功')).toBeVisible();
});
二十六、SSR 和水合场景的 E2E 测试
SSR 应用除了要验证 HTML 能返回,还要验证水合后页面可以交互。很多 SSR 问题不会在单元测试里暴露,例如服务端和客户端渲染结果不一致、首屏按钮看得见但点不了、路由切换后状态丢失等。
SSR E2E 测试重点:
- 首屏 HTML 是否包含核心内容。
- 水合后按钮、链接、表单是否可交互。
- 客户端路由跳转是否正常。
- 服务端数据和客户端数据是否一致。
- 错误页和重定向是否正确。
示例:
ts
import { test, expect } from '@playwright/test';
test('SSR 页面水合后可以点击按钮', async ({ page }) => {
await page.goto('/ssr-page');
await expect(page.getByRole('heading', { name: '商品详情' })).toBeVisible();
await page.getByRole('button', { name: '加入购物车' }).click();
await expect(page.getByText('已加入购物车')).toBeVisible();
});
二十七、微前端场景的 E2E 测试
微前端应用的 E2E 测试要关注主应用和子应用之间的集成问题,例如路由分发、样式隔离、登录态传递、全局状态通信、资源加载失败等。
测试重点:
- 子应用是否能被正确加载。
- 主子应用路由是否同步。
- 子应用刷新后是否还能恢复。
- 登录态是否正确传递。
- 子应用卸载后副作用是否清理。
- 主子应用样式是否互相污染。
二十八、端内 H5 和 WebView 场景
端内 H5 的 E2E 测试更复杂,因为页面运行在 App WebView 中,可能依赖 JSBridge、端能力、UA、登录态注入、主题模式、返回按钮等。
如果无法在真实 App 内自动化,可以分层处理:
- Web 层 E2E 验证页面主流程。
- Mock JSBridge 验证端能力调用。
- 少量真机自动化验证关键路径。
- 发布前人工验收高风险端能力。
JSBridge Mock 示例:
ts
await page.addInitScript(() => {
window.Bridge = {
getUserInfo() {
return Promise.resolve({ id: '1', name: '测试用户' });
},
closePage() {
window.__bridgeClosed = true;
}
};
});
await page.goto('/h5-page');
await expect(page.getByText('测试用户')).toBeVisible();
二十九、E2E 测试报告
好的 E2E 报告不仅告诉你失败了,还应该帮助你快速定位为什么失败。
报告应包含:
- 失败用例名称。
- 失败步骤。
- 错误堆栈。
- 截图。
- 视频。
- Trace。
- 控制台日志。
- 网络请求。
- 运行环境和浏览器版本。
三十、从零落地 E2E 测试的步骤
对于已有项目,不建议一开始就追求大而全。可以从一条最重要的主链路开始。
落地路径:
- 选定工具,例如 Playwright。
- 接入项目启动和测试命令。
- 写通第一条登录或首页访问用例。
- 接入 CI 并保存报告。
- 选择 3 到 5 条核心业务链路。
- 设计测试数据准备和清理方式。
- 治理不稳定用例。
- 再逐步扩展覆盖范围。
三十一、推荐的项目实践规范
1. 命名规范
text
tests
├── auth
│ └── login.spec.ts
├── order
│ └── create-order.spec.ts
├── pages
│ ├── LoginPage.ts
│ └── OrderPage.ts
└── fixtures
└── users.ts
2. package.json 脚本
json
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report"
}
}
3. 用例编写规范
- 一个用例只验证一个清晰业务目标。
- 用例名称要描述用户行为。
- 不依赖其他用例执行结果。
- 避免固定等待时间。
- 失败时能从报告看出上下文。
- 不把临时调试代码提交进仓库。
- 关键流程优先,边角流程克制。
三十二、完整示例:订单创建 E2E
下面是一个更接近真实业务的示例:通过 API 准备商品,通过页面完成下单,然后断言订单详情。
ts
import { test, expect } from '@playwright/test';
test('用户可以创建订单', async ({ page, request }) => {
const productName = `测试商品-${Date.now()}`;
const productResponse = await request.post('/api/test/products', {
data: {
name: productName,
price: 99
}
});
const product = await productResponse.json();
await page.goto(`/products/${product.id}`);
await expect(page.getByRole('heading', { name: productName })).toBeVisible();
await page.getByRole('button', { name: '加入购物车' }).click();
await page.getByRole('link', { name: '去购物车' }).click();
await expect(page.getByText(productName)).toBeVisible();
await page.getByRole('button', { name: '提交订单' }).click();
await expect(page.getByText('订单提交成功')).toBeVisible();
await expect(page).toHaveURL(/\/orders\/\d+/);
});
对应流程:
这个例子里,前置数据通过 API 创建,用户关键动作通过 UI 完成。这样既避免了冗长的前置步骤,又能验证真正重要的用户路径。
三十三、E2E 测试检查清单
上线前可以用下面的清单检查 E2E 体系是否健康:
- 是否覆盖了最高价值的主链路。
- 是否能在本地一条命令运行。
- 是否能在 CI 稳定运行。
- 是否保存失败截图、视频和 Trace。
- 是否避免了大量固定等待。
- 是否使用稳定选择器。
- 是否有清晰的数据准备和清理策略。
- 是否控制了用例数量和执行时间。
- 是否区分真实后端测试和 Mock 测试。
- 是否定期治理 flaky 用例。
三十四、总结
前端端到端界面测试的目标,不是把所有代码都测一遍,而是证明用户最重要的路径真的可用。它从浏览器出发,穿过 UI、路由、状态、接口、权限和后端环境,最终用用户能看到的结果来判断系统是否正常。
落地 E2E 测试时,最重要的是少而稳:优先主链路,使用语义选择器,避免固定等待,管理好测试数据,保留失败现场,并把它接入 CI。单元测试负责局部确定性,组件测试负责局部交互,E2E 测试负责关键业务信心。三者配合,前端质量体系才会真正可靠。