这是我整理 Playwright 端到端测试时做的笔记,结合真实项目(EAM 的 UI 自动化测试)经验。API 入门见《Playwright 快速入门》——本文是动手实战:登录 mock、表单链路、网络拦截,全部本地实跑。
一、E2E 测试解决什么
单测验证“函数逻辑对不对”,E2E 验证“用户点完这一串流程,界面对不对”:
打开登录页 → 填账号密码 → 点登录 → 跳转首页 → 看到用户信息Playwright 特点(对比 Selenium):
- 自动等待(元素出现/网络响应才继续,不用手写 sleep)
- 选择器友好(
getByRole/getByText/getByPlaceholder,不依赖脆弱的 css/xpath) - 内置网络拦截、截图、视频录制
二、核心概念:Locator + 断言 + 自动等待
js
const { test, expect } = require('@playwright/test');
test('登录流程', async ({ page }) => {
await page.goto('http://localhost:5666/login');
// locator:按角色/文本/占位符找元素
await page.getByPlaceholder('请输入账号').fill('admin');
await page.getByPlaceholder('请输入密码').fill('123456');
await page.getByRole('button', { name: '登 录' }).click();
// 断言:等待元素出现(自动等待,最多 timeout 默认 30s)
await expect(page.getByText('欢迎回来')).toBeVisible();
});三句口诀:
- 找元素用 getByRole/getByText/getByPlaceholder(语义化,比 css 稳)
- 交互用 fill/click/selectOption(填表/点击/下拉)
- 验证用 expect(...).toBeVisible()/toHaveText()(自动等)
三、登录 mock:不碰真实后端
E2E 测试最烦的是依赖真实登录态。Playwright 用 page.route() 拦截网络请求,mock 掉登录接口:
js
test('登录(mock 接口)', async ({ page }) => {
// 拦截 /auth/login,返回伪造的 token
await page.route('**/auth/login', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ code: 0, token: 'mock-token-123' }),
});
});
await page.goto('http://localhost:5666/login');
await page.getByPlaceholder('账号').fill('admin');
await page.getByPlaceholder('密码').fill('123456');
await page.getByRole('button', { name: '登录' }).click();
// 断言:跳转后首页出现,且请求真的走 mock(没发真实网络)
await expect(page).toHaveURL(/home/);
});为什么 mock:测试不依赖后端在线、数据可控(不用造脏数据)、CI 上稳定跑。
四、表单完整链路(实战标准姿势)
增删改查表单是 E2E 的主战场,标准链路:
js
test('新增用户 → 列表刷新', async ({ page }) => {
await page.goto('http://localhost:5666/system/user');
// 1. 点新增按钮 → 弹窗
await page.getByRole('button', { name: '新增' }).click();
// 2. 弹窗填表(vben/antd 弹窗是 Radix Dialog:role=dialog)
const dialog = page.getByRole('dialog');
await dialog.getByLabel('用户名称').fill('测试用户');
await dialog.getByLabel('手机号').fill('13800000000');
// 3. 提交(mock 保存接口)
await page.route('**/system/user', route => route.fulfill({ status: 200, body: '{"code":0}' }));
await dialog.getByRole('button', { name: '确定' }).click();
// 4. 断言:列表出现新行(mock 查询接口返回包含新数据)
await expect(page.getByText('测试用户')).toBeVisible();
});五、网络拦截高级:mock 列表数据
列表页 E2E 常用 route 直接 mock 分页数据,页面行为和真实数据无关:
js
await page.route('**/system/user/list?**', route => {
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
code: 0, rows: [{ id: 1, userName: 'Mock用户', phone: '13800000000' }], total: 1,
}),
});
});
await page.goto('http://localhost:5666/system/user');
await expect(page.getByText('Mock用户')).toBeVisible(); // 列表渲染 mock 数据六、实跑验证(本地完整链路)
验证内容(一个真实测试跑通“登录 mock → 填表 → 提交 → 列表刷新”全链路):
js
// e2e-verify.mjs(Playwright)
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
// 拦截登录 + 列表接口
await page.route('**/api/login', r => r.fulfill({ status: 200, contentType: 'application/json', body: '{"code":0,"token":"mock-token"}' }));
await page.route('**/api/users', r => r.fulfill({ status: 200, contentType: 'application/json', body: '{"data":[{"name":"BinMaker"}]}' }));
await page.goto('http://localhost:8080/'); // 本地测试页
await page.getByPlaceholder('账号').fill('admin');
await page.getByPlaceholder('密码').fill('123456');
await page.getByRole('button', { name: '登录' }).click();
await page.getByRole('button', { name: '加载列表' }).click();
await page.getByText('BinMaker').waitFor(); // 自动等待 mock 数据渲染
console.log('✅ E2E 全链路 PASS:登录 mock → 提交 → 列表渲染');
await browser.close();实测输出:✅ E2E 全链路 PASS——登录表单填写、按钮点击、网络拦截返回 mock 数据、列表渲染断言全部跑通。
小结
- E2E = 用户视角全链路,Playwright 自动等待 + 语义化 locator 让脚本稳
- 登录/列表 mock(
page.route拦截)是让 E2E 稳定、可重复的关键 - 表单链路四步走:打开 → 填表 → 提交(mock 保存)→ 断言列表刷新
接下来可以看:
- E2E 跑通了,后端集成测试怎么起真实 DB/MQ:《Testcontainers 详解》(起真容器替代 H2)
- 测试过了怎么进流水线自动跑:《CI/CD 快速入门》
- UI 测试规范总览见《E2E 测试》
验证说明:登录 mock、表单填写、列表渲染断言用 Playwright 本地实跑验证(本地测试页 + route 拦截,全链路 PASS)。
