集成测试与 E2E 测试
单元测试验证"零件"正确,集成测试验证"零件咬合"正确,E2E 测试则验证"整台机器按用户期望运转"。本文先讲清三者的边界,再分别上手 Cypress 与 Playwright 两大主流工具,最后给出金字塔平衡策略与 CI 落地方法。
一、集成测试 vs 单元测试 vs E2E
| 维度 | 单元测试 | 集成测试 | E2E 测试 |
|---|---|---|---|
| 验证对象 | 单个函数/组件 | 模块间协作、API 契约 | 用户完整操作流程 |
| 运行环境 | Node/jsdom 内存环境 | 内存环境 + 真实依赖 | 真实浏览器 |
| 依赖隔离 | 全部 mock | 隔离外部,联通内部 | 全部真实(可 mock 网络) |
| 速度 | 毫秒级 | 秒级 | 秒~分钟级 |
| 稳定性 | 最稳 | 较稳 | 易受环境与动画影响 |
| 修复成本 | 最低 | 中等 | 最高(要定位链路) |
| 适合发现 | 逻辑错误 | 接口契约、数据流错误 | 流程断裂、UI 回归 |
三者是互补关系而非替代关系:同一段代码,单元测试保证函数正确,集成测试保证拼接正确,E2E 保证用户在真实浏览器里走得通。
二、E2E 测试的价值与挑战
价值:以用户视角覆盖关键业务路径(登录、下单、支付),防回归能力最强;产出的测试报告就是"可执行的验收文档"。
挑战:
| 挑战 | 说明 | 应对 |
|---|---|---|
| 运行慢 | 启动浏览器 + 页面渲染 | 只覆盖关键路径,并行分片 |
| 不稳定 | 网络、动画、时序导致偶发失败 | 自动重试、等待元素而非 sleep |
| 依赖真实环境 | 数据、权限、第三方服务 | 测试环境 + mock 网络层 |
| 维护成本高 | 选择器变更即测试失效 | data-testid 稳定定位 |
三、Cypress 入门
1. 安装与启动
npm install --save-dev cypress
npx cypress open # 打开图形化运行器
npx cypress run # 命令行无头运行2. 测试结构
// cypress/e2e/login.cy.js
describe("登录流程", () => {
beforeEach(() => {
cy.visit("http://localhost:5173/login"); // 打开页面
});
it("输入正确账号密码可登录成功", () => {
cy.get('[data-testid="username"]').type("admin");
cy.get('[data-testid="password"]').type("123456");
cy.get('[data-testid="submit"]').click();
cy.url().should("include", "/dashboard"); // 断言跳转
cy.contains("欢迎回来").should("be.visible");
});
});3. 常用命令一览
| 命令 | 作用 |
|---|---|
cy.visit(url) | 打开页面 |
cy.get(selector) | 按选择器查找元素 |
.type(text) | 输入文本 |
.click() | 点击 |
.select(value) | 选择下拉项 |
.check() / .uncheck() | 勾选/取消复选框 |
cy.contains(text) | 按文本找元素 |
cy.intercept(url, stub) | 拦截并 stub 网络请求 |
cy.wait("@alias") | 等待某个请求完成 |
.should("be.visible") | 断言可见性 |
.should("have.text", "...") | 断言文本 |
Cypress 所有命令自动重试等待元素出现,无需手动 sleep。配合 cy.intercept 可以稳定控制后端数据:
cy.intercept("GET", "/api/products", { fixture: "products.json" }).as("getProducts");
cy.visit("/shop");
cy.wait("@getProducts"); // 等待 stub 请求完成
cy.get('[data-testid="product"]').should("have.length", 3);四、Playwright 入门
1. 安装:三大浏览器开箱即用
npm init playwright@latest # 交互式初始化
npx playwright install # 安装 Chromium / Firefox / WebKit
npx playwright test # 运行测试2. 测试结构
// tests/login.spec.js
const { test, expect } = require("@playwright/test");
test("登录成功跳转首页", async ({ page }) => {
await page.goto("http://localhost:5173/login");
await page.getByTestId("username").fill("admin");
await page.getByTestId("password").fill("123456");
await page.getByTestId("submit").click();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByText("欢迎回来")).toBeVisible();
});3. 常用 API 与特色能力
| API | 作用 |
|---|---|
page.goto(url) | 打开页面 |
page.getByRole / getByText / getByTestId | 语义化定位(推荐) |
page.waitForSelector(sel) | 等待元素出现 |
page.waitForLoadState("networkidle") | 等待网络空闲 |
page.screenshot({ path }) | 截屏 |
page.locator(sel).click() | 定位并点击 |
test.describe.configure({ retries }) | 配置重试 |
test.use({ viewport }) | 模拟视口尺寸 |
// 截屏 + 等待元素的典型组合
await page.goto("https://example.com");
await page.waitForSelector("h1"); // 等待特定元素
await page.screenshot({ path: "shots/home.png", fullPage: true });// 多浏览器并行:一次编写,三大浏览器全跑
test.use({ project: ["chromium", "firefox", "webkit"] });五、Cypress vs Playwright 对比
| 对比维度 | Cypress | Playwright |
|---|---|---|
| 支持的浏览器 | Chrome 系为主 | Chromium / Firefox / WebKit |
| 运行模型 | 与页面同进程(JavaScript 执行在页面内) | 独立控制浏览器(无头/有头) |
| 多标签页/多窗口 | 较弱 | 原生支持 |
| 网络拦截 | cy.intercept | page.route |
| 并行执行 | 付费版/第三方 | 原生支持 worker 并行 |
| 调试体验 | 时间轴回放、GUI 友好 | trace viewer 记录完整轨迹 |
| 学习曲线 | 平缓、资料多 | 稍陡但 API 更现代 |
| 社区生态 | 非常成熟 | 快速增长 |
选型建议:追求调试体验与上手速度选 Cypress;需要三浏览器覆盖、多标签场景、大规模并行选 Playwright。两者都是行业标准,核心能力(定位、断言、拦截、截图)思路一致。
六、测试金字塔与平衡策略
| 层级 | 占比建议 | 关键路径 | 运行频率 |
|---|---|---|---|
| 单元测试 | 70% | 业务函数、组件逻辑 | 每次提交 |
| 集成测试 | 20% | 模块协作、API 层 | 每次提交 |
| E2E 测试 | 10% | 登录、购物、支付 | 每次合并前/每晚 |
平衡要点:E2E 是"信号灯"不是"防线"——它用少量关键用例守住整体流程,真正的防线在底层单元测试。当 E2E 用例超过 100 个且频繁脆弱失败时,应回头审视:是不是该下沉为集成测试,或者选择器不够稳定。
七、CI 集成:GitHub Actions 中运行 E2E
1. 标准工作流
# .github/workflows/e2e.yml
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
# 启动应用(供 E2E 访问)
- run: npm run build
- run: npm run preview -- --port 4173 &
- run: npx playwright install --with-deps
- run: npx playwright test
# 失败时上传截图与报告
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/2. 报告与失败排查
- Playwright:
npx playwright show-report打开 HTML 报告;失败自动截图与录屏。 - Cypress:
cypress run产出视频与截图,配合mochawesome生成 HTML 报告。 - CI 中断言:
cypress run或playwright test非零退出码即代表失败,Actions 会标红并保留产物。
八、可测试性设计
1. 稳定选择器:data-testid
UI 文案会改、class 会被重构,因此用专为测试设计的属性定位:
<!-- 差:依赖 class 与文案,极易变化 -->
<button class="btn btn-primary">立即购买</button>
<!-- 好:data-testid 稳定且语义明确 -->
<button class="btn btn-primary" data-testid="buy-now">立即购买</button>// 差:脆弱的嵌套 CSS 路径
cy.get(".product-card .header .buy-btn");
// 好:语义化定位
cy.get('[data-testid="buy-now"]');| 选择器优先级 | 示例 | 稳定性 |
|---|---|---|
| 语义化定位 | getByRole("button", { name: "购买" }) | 高 |
| data 属性 | getByTestId("buy-now") | 高 |
| 文本内容 | cy.contains("购买") | 中 |
| class/id | .btn-primary | 低(易变) |
| CSS 嵌套路径 | div > ul > li > a | 极低(勿用) |
2. 等待策略
| 做法 | 评价 |
|---|---|
固定 sleep(2000) | 反面教材:快则浪费、慢则偶发失败 |
| 显式等待元素 | cy.get(...).should("be.visible") / waitForSelector |
| 等待网络请求 | cy.wait("@getProducts") |
九、端到端测试场景实战
1. 登录流程(含失败与成功两条路径)
// playwright 示例
test.describe("登录", () => {
test("密码错误提示失败", async ({ page }) => {
await page.goto("/login");
await page.getByTestId("username").fill("admin");
await page.getByTestId("password").fill("wrong");
await page.getByTestId("submit").click();
await expect(page.getByTestId("error-msg")).toHaveText("账号或密码错误");
});
test("成功后跳转并展示用户信息", async ({ page }) => {
await page.goto("/login");
await page.getByTestId("username").fill("admin");
await page.getByTestId("password").fill("123456");
await page.getByTestId("submit").click();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByTestId("user-name")).toHaveText("管理员");
});
});2. 购物车全流程
// cypress 示例:加购 → 结算 → 下单成功
describe("购物车", () => {
it("完成一次下单", () => {
cy.intercept("POST", "/api/orders", { statusCode: 201 }).as("createOrder");
cy.visit("/shop");
cy.get('[data-testid="product-1"]').find('[data-testid="add-cart"]').click();
cy.get('[data-testid="cart-count"]').should("have.text", "1");
cy.get('[data-testid="cart-icon"]').click();
cy.get('[data-testid="checkout"]').click();
cy.get('[data-testid="confirm"]').click();
cy.wait("@createOrder");
cy.get('[data-testid="order-success"]').should("be.visible");
cy.get('[data-testid="cart-count"]').should("have.text", "0");
});
});端到端场景的编写要点:一条用例走完一个完整业务闭环(用户视角的开始到结束),中途的每一步都用稳定选择器定位,外部依赖(支付、短信)通过拦截 stub 掉,保证测试在任何环境可重复执行。
小结
集成测试与 E2E 测试是质量保障体系中"靠近用户"的两层:集成测试守护模块协作的契约,E2E 测试守护业务主流程的完整闭环。掌握 Cypress 与 Playwright 之后,配合 data-testid 稳定定位、显式等待策略、以及 GitHub Actions 中的自动化运行与报告产出,就能把"上线前人工点一遍"升级为"每次合并自动全流程验证"。