返回自动化工程模块从业务风险到可持续回归 每一层只承担一种职责 从上到下选择定位方式
业务用例调用页面能力,页面对象不决定业务是否通过 每条用例拥有完整的数据生命周期 等待真正的业务条件成立
一次 E2E 验证连接三个观察面 共享稳定上下文,隔离可变业务数据 失败报告至少留下五类证据
Automation / Tutorial 14
Playwright 自动化测试教程
把“浏览器能自动点击”升级为“核心业务可以稳定、独立、可诊断地持续回归”。
10 个章节商城下单 E2ETypeScript + CI
01
先确定哪些下单流程值得自动化
风险优先你要守住的核心流程
用户登录商城,选择商品,填写地址,使用优惠券并提交订单。自动化测试需要确认页面操作成功、订单金额正确、订单只创建一次,而且失败时能留下足够证据。
选择场景高风险且重复
准备数据独立且可恢复
执行浏览器模拟真实用户
检查结果页面、接口和数据
保留证据报告与 trace
适合优先自动化
- 每天或每次发布都要执行的主流程。
- 金额、库存、订单状态等高风险规则。
- 步骤稳定、结果可以明确判断的场景。
- 人工执行耗时且容易遗漏的重复回归。
暂时不要急着自动化
- 需求仍在频繁变化的临时页面。
- 只执行一次且没有长期回归价值的场景。
- 结果依赖主观视觉判断、尚无明确标准的功能。
- 无法准备独立数据、执行后会污染生产的操作。
自动化不是把所有人工用例翻译成脚本。先覆盖高风险、重复执行且结果稳定的场景,脚本才会持续产生价值。
02
搭建一套能长期维护的项目结构
职责分开测试用例表达业务场景
页面对象封装页面操作
Fixture提供身份与上下文
数据层准备和清理数据
项目目录
tests/
checkout.spec.ts # 用例只表达业务场景
pages/
checkout.page.ts # 页面元素和操作
fixtures/
authenticated.ts # 登录状态与公共上下文
data/
checkout.ts # 商品、地址和优惠券数据
playwright.config.ts # 浏览器、报告与失败证据先写最小可运行用例
tests/checkout.spec.ts
import { test, expect } from '@playwright/test';
test('当库存充足且地址有效时,订单创建成功', async ({ page }) => {
await page.goto('/checkout');
await page.getByLabel('购买数量').fill('1');
await page.getByRole('button', { name: '提交订单' }).click();
await expect(page.getByText('下单成功')).toBeVisible();
await expect(page.getByTestId('order-id')).not.toBeEmpty();
});03
用用户看得懂的方式定位元素
稳定优先01角色与标签
最接近用户语义
02稳定业务文案
确认唯一且不易变化
03测试标识
复杂组件的团队约定
04CSS / XPath
旧系统最后选择
定位方式的选择顺序
| 级别 | 推荐方式 | 适用位置 | 判断依据 |
|---|---|---|---|
| 优先 | getByRole / getByLabel | 按钮、输入框和对话框 | 接近用户和无障碍语义,页面改版后更稳定 |
| 其次 | getByText / getByPlaceholder | 稳定且唯一的业务文案 | 可读性好,但需要确认多语言和文案变化 |
| 约定 | getByTestId | 没有稳定语义的复杂组件 | 需要团队维护测试标识 |
| 谨慎 | CSS / XPath | 无法增加语义或测试标识的旧页面 | 容易依赖 DOM 层级和样式类 |
稳定定位示例
await page.getByRole('button', { name: '提交订单' }).click();
await page.getByLabel('手机号').fill('13800138000');
await page.getByTestId('coupon-card').filter({ hasText: '满100减20' }).click();不要用
.form > div:nth-child(3) button 记录页面结构。优先描述“用户要操作什么”,页面小幅改版后脚本才不容易失效。04
用页面对象封装操作,不隐藏业务判断
页面与用例分层用例条件与预期
页面对象定位与操作
浏览器页面真实交互
断言业务结果
pages/checkout.page.ts
import { expect, type Page } from '@playwright/test';
export class CheckoutPage {
constructor(private readonly page: Page) {}
async open() {
await this.page.goto('/checkout');
}
async fillAddress(phone: string) {
await this.page.getByLabel('手机号').fill(phone);
}
async submit() {
await this.page.getByRole('button', { name: '提交订单' }).click();
}
async expectSuccess() {
await expect(this.page.getByText('下单成功')).toBeVisible();
}
}页面对象负责“在哪里操作、怎样操作”;测试用例负责“为什么这样操作、应该得到什么业务结果”。不要把所有断言都塞进一个万能方法。
05
让每条用例拥有独立测试数据
可重复执行生成标识避免名称冲突
准备数据接口快速创建
执行用例独立完成场景
记录结果保存业务 ID
清理隔离恢复测试环境
执行前准备
- 通过测试接口创建商品、库存和优惠券。
- 使用唯一业务标识,避免并行用例相互覆盖。
- 只准备当前用例真正需要的数据。
- 不要依赖上一条用例执行成功。
执行后处理
- 记录订单 ID,便于失败时定位。
- 测试环境允许时清理临时数据。
- 无法删除的业务数据使用专用租户或日期前缀隔离。
- 禁止在生产环境运行创建、支付或删除类自动化。
使用唯一数据
const runId = Date.now() + '-' + testInfo.workerIndex;
const addressName = 'E2E-' + runId;
await request.post('/test-support/addresses', {
data: { name: addressName, phone: '13800138000' }
});06
等待业务结果,而不是等待固定秒数
自动重试断言触发操作提交订单
等待响应创建接口完成
等待页面成功状态出现
轮询业务订单状态落稳
等待与断言
// 不推荐:页面快慢变化后容易偶发失败
await page.waitForTimeout(3000);
// 推荐:等待真正的业务结果出现
await expect(page.getByText('下单成功')).toBeVisible();
await expect(page.getByTestId('order-status')).toHaveText('待支付');
await expect.poll(async () => getOrder(orderId)).toMatchObject({ status: 'PENDING_PAYMENT' });一条下单用例需要检查的结果
| 层级 | 检查内容 | 回答的问题 |
|---|---|---|
| 页面 | 成功提示、按钮状态、订单号 | 用户是否看到正确结果 |
| 接口 | 响应状态、业务码、关键字段 | 请求是否按契约完成 |
| 业务 | 金额、优惠、库存和订单状态 | 核心规则是否正确 |
| 数据 | 订单只创建一次,库存只扣减一次 | 跨层结果是否保持一致 |
07
让页面操作和接口结果互相印证
UI 不等于全部页面操作用户提交订单
接口响应获得订单 ID
页面结果展示相同订单
业务数据金额与库存正确
监听创建订单响应
const createOrderResponse = page.waitForResponse((response) =>
response.url().includes('/api/orders') && response.request().method() === 'POST'
);
await checkout.submit();
const response = await createOrderResponse;
const body = await response.json();
expect(response.status()).toBe(200);
expect(body.orderId).toBeTruthy();
await expect(page.getByTestId('order-id')).toHaveText(body.orderId);什么时候拦截接口
- 第三方服务不稳定且不是本次验证目标。
- 需要稳定复现超时、500 或特殊响应。
- 需要验证前端在异常响应下的提示与恢复。
- 不能用 Mock 代替所有真实集成回归。
08
复用登录状态,同时保持用例隔离
Fixture登录状态可复用
浏览器上下文每条用例独立
订单数据每条用例新建
执行结束保存或清理
复用已登录上下文
import { test as base } from '@playwright/test';
export const test = base.extend({
storageState: 'playwright/.auth/user.json',
});
test.beforeEach(async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('link', { name: '我的订单' })).toBeVisible();
});可以复用登录状态,但不要让不同用例共享购物车、订单或可变库存。身份可以复用,业务数据应尽量独立。
09
让失败结果能够快速定位
证据链Trace
操作、DOM 与网络
截图
失败瞬间页面
视频
完整操作过程
请求日志
接口输入与响应
业务 ID
追踪后端数据
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
reporter: [['html', { open: 'never' }]],
retries: process.env.CI ? 2 : 0,
});先判断失败属于哪一类
- 产品缺陷:实际业务结果与预期不一致。
- 脚本缺陷:定位、等待或数据准备不正确。
- 环境问题:服务、网络或依赖不可用。
- 偶发问题:需要用 trace 和日志找到真实触发条件。
重试不能掩盖问题
- 重试通过仍要记录首次失败原因。
- 持续偶发的用例需要修复,而不是无限增加重试。
- 报告中保留 trace、截图、视频、请求和业务 ID。
- 只有确认环境抖动时才把用例标记为不稳定。
10
把稳定用例放进持续集成
分层回归按反馈速度组织自动化任务
| 触发时机 | 执行范围 | 目标时长 | 通过标准 |
|---|---|---|---|
| 提交前 | 冒烟用例 | 1~3 分钟 | 核心页面可以打开,主流程没有立即阻塞 |
| 合并请求 | 核心回归 | 5~15 分钟 | P0/P1 场景、接口依赖和主要浏览器通过 |
| 定时任务 | 完整回归 | 按项目规模 | 更广的浏览器、数据组合和低频场景 |
CI 命令
npx playwright test --project=chromium --grep @smoke
npx playwright test --project=chromium
npx playwright show-report练习:完成下单 E2E 项目
- 选择 3 条 P0 下单用例,说明为什么值得自动化。
- 创建 CheckoutPage,只封装页面元素和重复操作。
- 通过接口准备独立商品、库存、地址和优惠券。
- 用语义定位完成下单,不使用依赖层级的 XPath。
- 同时断言页面结果、创建订单响应和关键业务数据。
- 复现接口超时,验证用户可以安全重试且不会重复下单。
- 开启 trace、截图和 HTML 报告,故意制造失败并完成一次定位。
- 把冒烟用例接入 CI,并设置明确的运行环境和失败退出条件。
脚本稳定
- 定位语义清楚
- 没有固定等待
- 数据彼此隔离
- 重复执行结果一致
结果可信
- 关键业务有断言
- 接口与页面一致
- 失败保留证据
- 重试不掩盖缺陷
可以持续运行
- 用例按标签分层
- 环境参数外置
- CI 失败会阻断
- 报告可以追溯
完成页面自动化后,继续把冒烟、回归、报告和失败治理接入持续测试流水线。
继续学习持续测试与 CI/CD