什么是端到端测试
端到端测试(End-to-End Testing,简称 E2E)从真实用户视角出发,模拟完整的操作流程——打开页面、点击、输入、跳转、提交,验证整个应用(前端 + 后端 + 数据库)集成后能否按预期工作。
与单元测试、集成测试相比,E2E 测试更接近真实使用场景,能发现跨模块、跨系统的集成问题,是保障核心业务链路稳定的最后一道防线。
| 测试类型 | 测试对象 | 速度 | 覆盖 |
|---|---|---|---|
| 单元测试 | 单个函数/组件 | 快 | 局部逻辑 |
| 集成测试 | 模块间交互 | 中 | 接口对接 |
| 端到端测试 | 完整业务流程 | 慢 | 用户核心路径 |
💡 提示
测试金字塔建议:单元测试为主,集成测试为辅,E2E 测试挑选 最关键、最频繁 的几条业务链路重点覆盖。主流框架对比
目前最常用的两款 E2E 测试框架是 Playwright 与 Cypress,均支持自动等待、截图、视频录制与多浏览器运行。
| 能力 | Playwright | Cypress |
|---|---|---|
| 多浏览器 | Chromium / Firefox / WebKit | Chromium / Firefox / Edge |
| 多标签页/多窗口 | ✅ 原生支持 | ⚠️ 受限 |
| 网络拦截 | ✅ 强大(Mock API) | ✅ 支持 |
| 移动端模拟 | ✅ 内置设备模拟 | 需额外配置 |
| 语言 | JS/TS/Python/Java/.NET | JavaScript/TypeScript |
✅ 建议
新项目推荐 Playwright:生态更完整、跨浏览器能力更强;已有 JS 团队且偏重调试体验可选 Cypress。编写第一个 E2E 测试
以 Playwright 为例,先初始化项目并安装依赖:
npm init -y
npm i -D @playwright/test
npx playwright install
在 tests/ 目录新建测试文件,模拟用户完成一次登录并跳转首页:
const { test, expect } = require("@playwright/test");
test("用户登录后进入首页", async ({ page }) => {
// 打开登录页
await page.goto("https://example.com/login");
// 填写表单并提交
await page.fill("#username", "admin");
await page.fill("#password", "123456");
await page.click("button[type=submit]");
// 等待跳转并断言
await expect(page).toHaveURL(/\/home/);
await expect(page.locator(".welcome")).toContainText("欢迎回来");
});
运行测试并生成 HTML 报告:
npx playwright test --reporter=html
✅ 最佳实践
- 用 data-testid 定位元素,避免依赖易变的 CSS 类名。
- 测试数据独立管理,避免依赖线上真实数据。
- 接入 CI:每次提交后自动跑核心链路用例。
定位器与常用操作
定位器(Locator)是 Playwright 中查找元素的统一方式,推荐按可访问性或 角色定位,而不是脆弱的 CSS 选择器。
// 按文本 / 角色定位(推荐)
await page.getByRole("button", { name: "提交" }).click();
await page.getByText("用户名").fill("admin");
// 按 testid 定位(页面需添加 data-testid 属性)
await page.getByTestId("search-input").fill("vue");
// 按选择器定位(兜底)
await page.locator("form input[type=password]").fill("123456");
常用操作
await page.goto("https://example.com"); // 跳转
await page.fill("#name", "张三"); // 输入
await page.click("text=登录"); // 点击
await page.selectOption("#city", "beijing"); // 下拉选择
await page.check("#agree"); // 勾选
await page.hover(".menu"); // 悬停
await page.keyboard.press("Enter"); // 键盘
await page.screenshot({ path: "shot.png" }); // 截图
⚠️ 注意
优先 getByRole / getByText;避免直接使用 xpath 与绝对 CSS 路径,维护成本高。网络拦截与 Mock 接口
E2E 测试中常需要拦截网络请求:mock 第三方接口、模拟慢网络、断言请求参数,让测试稳定且可复现。
Mock 一个接口
test("登录接口被 Mock", async ({ page }) => {
await page.route("**/api/login", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ token: "fake-token", user: { name: "admin" } }),
});
});
await page.goto("/login");
await page.fill("#username", "admin");
await page.fill("#password", "wrong-password");
await page.click("button[type=submit]");
// 即使密码错误,也因为 Mock 而登录成功
await expect(page).toHaveURL(/\/home/);
});
断言请求参数
test("提交表单时携带正确参数", async ({ page }) => {
let requestBody = null;
await page.route("**/api/submit", (route) => {
requestBody = route.request().postData();
route.continue();
});
await page.goto("/form");
await page.fill("#email", "a@b.com");
await page.click("button[type=submit]");
await page.waitForTimeout(500);
expect(requestBody).toContain("a@b.com");
});
模拟慢网络
// 只对图片资源模拟慢速
await page.route("**/*.png", async (route) => {
await new Promise((r) => setTimeout(r, 2000));
route.continue();
});
✅ 建议
只 Mock 第三方或不可控的接口;自己后端的核心链路尽量走真实请求,保证测试有效性。Cypress 快速上手
Cypress 以开发者体验著称:自带时间旅行调试、自动等待、直观的断言语法。
初始化
npm i -D cypress
npx cypress open # 打开交互式界面
编写用例
describe("登录流程", () => {
it("正确凭证可以登录", () => {
cy.visit("/login");
cy.get("#username").type("admin");
cy.get("#password").type("123456");
cy.get("button[type=submit]").click();
cy.url().should("include", "/home");
cy.contains(".welcome", "欢迎回来").should("be.visible");
});
it("密码错误提示错误信息", () => {
cy.visit("/login");
cy.get("#username").type("admin");
cy.get("#password").type("bad");
cy.get("button[type=submit]").click();
cy.contains(".error", "用户名或密码错误").should("be.visible");
});
});
关键命令
| 命令 | 作用 |
|---|---|
cy.visit(url) | 访问页面 |
cy.get(sel) | 获取元素(自动重试) |
cy.intercept() | 网络拦截 / Mock |
cy.fixture() | 加载测试数据文件 |
cy.screenshot() | 截图 |
⚠️ 注意
Cypress 默认不支持多标签页/跨域 iframe,多窗口场景建议用 Playwright。登录态与测试数据管理
E2E 测试最耗时的是反复登录与数据准备。正确做法是:复用登录态、按需造数据、跑完清理。
复用登录态(Playwright storageState)
// global-setup.js —— 先登录一次并保存 cookie
const { chromium } = require("@playwright/test");
module.exports = async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://example.com/login");
await page.fill("#username", "admin");
await page.fill("#password", "123456");
await page.click("button[type=submit]");
await page.waitForURL("**/home");
await page.context().storageState({ path: "auth.json" });
await browser.close();
};
// playwright.config.js 中配置
// use: { storageState: "auth.json" }
测试数据:独立且可清理
test("创建订单", async ({ page, request }) => {
// 通过 API 准备数据(比 UI 造数快且稳定)
const order = await request.post("/api/orders", { data: { amount: 99 } });
const orderId = (await order.json()).id;
// 走 UI 验证订单可见
await page.goto("/orders");
await expect(page.getByText("¥99.00")).toBeVisible();
// 清理数据
await request.delete(`/api/orders/${orderId}`);
});
✅ 最佳实践
- 优先用 API 造数据,而不是 UI 一步步点出来。
- 测试账号使用专用环境,避免污染生产数据。
- 用例独立:互相之间不依赖执行顺序。
接入 CI 持续集成
E2E 测试只有跑在 CI 里才有意义:每次代码提交自动执行,回归问题第一时间暴露。
GitHub Actions 示例
name: E2E Tests
on:
push:
branches: [ main ]
pull_request:
jobs:
e2e:
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 && npm run start &
- run: npx playwright test
env:
BASE_URL: http://localhost:3000
# 失败时保留报告与截图供排查
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
并行加速
// playwright.config.js —— 分片并行
export default {
workers: process.env.CI ? 4 : undefined,
fullyParallel: true,
};
// CI 中拆分多台机器
// npx playwright test --shard=1/4
⚠️ 注意
CI 中一定要 playwright install --with-deps,否则缺少系统依赖会导致浏览器启动失败。调试与最佳实践
E2E 用例不稳定时,善用 Playwright 的调试工具能快速定位问题。
调试手段
# 有头模式 + 慢动作,直观观察每一步
npx playwright test --headed --slow-mo=500
# 只跑单个用例
npx playwright test -g "登录"
# 失败后自动保留截图与 trace
npx playwright test --trace on
# 查看 Trace Viewer 分析失败原因
npx playwright show-trace test-results/**/trace.zip
让测试稳定的原则
| 原则 | 说明 |
|---|---|
| 避免固定等待 | 用 expect(...).toBeVisible() 自动等待,不用 sleep |
| 独立用例 | 每个用例自己准备数据,不依赖其他用例结果 |
| 语义化定位 | 用 role / testid,不用易变的样式类名 |
| 控制网络 | Mock 第三方服务,避免外部环境抖动导致失败 |
| 维护用例数量 | 只保留高价值核心链路,控制执行时长 |
💡 提示
E2E 的价值在于守住核心流程,不在于数量。100 个脆弱的用例不如 10 个稳定的核心链路用例。第 1 / 9 节