E2E 测试
约 1792 字大约 6 分钟
测试E2E端到端测试
2026-04-16
E2E 测试简介
端到端测试(End-to-End Testing,简称 E2E 测试)是一种从用户真实使用场景出发,验证整个应用系统流程的测试方法。E2E 测试模拟真实用户的操作行为,从应用的前端界面一路走到后端数据库,验证整个系统是否能够正常工作。
与其他测试类型的区别
| 类型 | 目标 | 速度 | 隔离性 | 关注点 |
|---|---|---|---|---|
| 单元测试 | 函数/组件 | 快 | 高 | 最小单位 |
| 集成测试 | 模块交互 | 中 | 中 | 模块间接口 |
| E2E 测试 | 完整流程 | 慢 | 低 | 用户真实场景 |
E2E 测试的定位和价值
真实性
E2E 测试的最大特点是真实性。它模拟的是真实用户在浏览器或应用中的实际操作,而不是孤立地测试某个函数或组件。这种真实性带来了以下优势:
- 验证真实的用户操作流程
- 发现单元测试和集成测试无法覆盖的问题
- 确认前端、后端、数据库的完整集成
覆盖率
E2E 测试能够验证整个系统的工作,发现系统集成过程中的问题:
- 路由配置是否正确
- API 接口是否正常对接
- 数据库读写是否正常
- 第三方服务集成是否生效
常用 E2E 测试工具
Playwright(推荐)
微软开发的现代 E2E 测试框架,支持所有现代浏览器,功能强大且易于使用。
特点:
- 原生支持 Chromium、Firefox、WebKit
- 自动等待机制,减少 flaky 测试
- 强大的选择器策略
- 内置 CI/CD 集成支持
Cypress
专为 Web 应用设计的 E2E 测试框架,提供出色的开发者体验。
特点:
- 实时重载和热重载
- 内置调试工具
- 自动截图和录制
- 丰富的内置断言
Selenium
最老牌的 Web 自动化测试框架,支持多语言、多浏览器。
特点:
- 语言支持广泛(Java、Python、C#、JavaScript 等)
- 浏览器支持全面
- 生态系统成熟
- 配置相对复杂
Playwright 入门
安装和配置
# 安装
npm init playwright@latest
# 或者在现有项目中添加
npm install -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,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});基本 API
import { test, expect } from '@playwright/test';
// 访问页面
test('homepage loads correctly', async ({ page }) => {
await page.goto('/');
// 点击元素
await page.click('button#submit');
// 填写表单
await page.fill('input[name="email"]', 'test@example.com');
// 获取文本内容
const title = await page.textContent('h1');
// 断言
expect(title).toBe('Welcome');
});选择器策略
// 文本选择器
await page.click('text=Submit');
await page.getByText('Sign in').click();
// 角色选择器(推荐)
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('test@example.com');
await page.getByPlaceholder('Enter password').fill('password');
// CSS 选择器
await page.click('.btn-primary');
await page.click('#submit-btn');
// 数据属性
await page.click('[data-testid="submit"]');
// XPath
await page.click('xpath=//button[@type="submit"]');
// 组合选择器
await page.click('button.primary[type="submit"]');常用操作
// 悬停
await page.hover('button.dropdown');
// 双击
await page.dblclick('td.cell');
// 右键菜单
await page.click('div.row', { button: 'right' });
// 键盘操作
await page.keyboard.press('Enter');
await page.keyboard.type('Hello World');
await page.keyboard.press('Control+a');
// 文件上传
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
// 拖拽
await page.dragAndDrop('#source', '#target');
// 滚动
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));断言
import { expect } from '@playwright/test';
// 元素存在
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('.loading')).toBeHidden();
// 文本内容
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('p')).toContainText('error');
// 计数
await expect(page.locator('li.item')).toHaveCount(5);
// 属性
await expect(page.locator('input')).toHaveValue('test@example.com');
await expect(page.locator('button')).toBeDisabled();
// URL
await expect(page).toHaveURL('/dashboard');
// Title
await expect(page).toHaveTitle('Dashboard');E2E 测试最佳实践
测试独立性和隔离
每个 E2E 测试应该独立运行,不依赖其他测试的结果:
test('user can register and login', async ({ page }) => {
// 1. 注册新用户
await page.goto('/register');
await page.fill('[name="username"]', `user_${Date.now()}`);
await page.fill('[name="email"]', `user_${Date.now()}@example.com`);
await page.fill('[name="password"]', 'StrongPass123!');
await page.click('[type="submit"]');
// 2. 验证注册成功并跳转到登录页
await expect(page).toHaveURL('/login');
// 3. 使用新账号登录
await page.fill('[name="email"]', `user_${Date.now()}@example.com`);
await page.fill('[name="password"]', 'StrongPass123!');
await page.click('[type="submit"]');
// 4. 验证登录成功
await expect(page).toHaveURL('/dashboard');
});建议:
- 每个测试使用唯一的数据(如带时间戳的用户名)
- 测试开始前清理必要的数据状态
- 使用
beforeEach准备测试环境 - 避免测试之间的隐式依赖
避免 Flaky 测试
Flaky 测试是指结果不稳定的测试,有时通过有时失败。以下是减少 flaky 测试的建议:
// 1. 使用自动等待,而非固定延时
// 不推荐
await page.waitForTimeout(1000);
await page.click('button');
// 推荐 - Playwright 的自动等待
await page.click('button'); // Playwright 会等待元素可点击
// 2. 使用合适的等待条件
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
// 3. 使用 locators 而不是 selectors
// 不推荐 - selector 可能过时
await page.click('button.submit-btn');
// 推荐 - locators 在执行时查找
await page.getByRole('button', { name: 'Submit' }).click();
// 4. 确保测试数据稳定
test.beforeEach(async ({ page, request }) => {
// 创建测试数据
const user = await request.post('/api/users', {
data: { name: 'Test User', email: `test_${Date.now()}@example.com` },
});
// 存储到测试上下文
test.info().user = await user.json();
});
test('user can view profile', async ({ page }) => {
await page.goto(`/users/${test.info().user.id}`);
await expect(page.getByText(test.info().user.name)).toBeVisible();
});页面对象模式
页面对象模式(Page Object Pattern)是一种将页面元素和操作封装为对象的测试设计模式:
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
get emailInput() {
return this.page.getByLabel('Email');
}
get passwordInput() {
return this.page.getByLabel('Password');
}
get submitButton() {
return this.page.getByRole('button', { name: 'Sign in' });
}
get errorMessage() {
return this.page.locator('.error-message');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// pages/DashboardPage.ts
export class DashboardPage {
constructor(private page: Page) {}
get welcomeMessage() {
return this.page.locator('h1');
}
get userMenu() {
return this.page.getByRole('button', { name: /user menu/i });
}
async logout() {
await this.userMenu.click();
await this.page.getByText('Sign out').click();
}
}
// 测试中使用
test('user can login and view dashboard', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await page.goto('/login');
await loginPage.login('test@example.com', 'password123');
await expect(dashboardPage.welcomeMessage).toBeVisible();
});优点:
- 页面元素集中管理,UI 变化时只需修改一处
- 测试代码更简洁易读
- 提高测试代码复用性
- 便于团队协作和维护
CI 集成
GitHub Actions
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Build application
run: npm run build
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7Docker 环境中的测试
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- NODE_ENV=test
command: npm run start
e2e:
image: mcr.microsoft.com/playwright:v1.40.0
depends_on:
- app
environment:
- BASE_URL=http://app:3000
volumes:
- ./e2e:/e2e
command: npx playwright test测试报告
Playwright 支持生成多种格式的测试报告:
// playwright.config.ts
export default defineConfig({
reporter: [
['html', { outputFolder: 'playwright-report' }],
['json', { outputFile: 'results.json' }],
],
});查看 HTML 报告:
npx playwright show-report总结
E2E 测试是质量保障体系中的重要一环,它能够:
- 从用户视角验证系统功能
- 发现集成阶段的问题
- 提供信心保障,让团队放心发布
选择合适的工具(推荐 Playwright),遵循最佳实践,可以有效提升 E2E 测试的可靠性和可维护性。
