Playwright 最佳实践
本章汇总 Playwright 测试编写和维护过程中最重要的最佳实践,帮助你编写稳定、可维护的测试。
测试哲学
测试用户可见行为
自动化测试应该验证最终用户能看到和交互的内容,而不是验证代码实现细节。
比如,测试应该验证页面上显示了正确的文本、按钮可以点击、表单提交后跳转到正确页面,而不是验证某个 JavaScript 函数的返回值或 DOM 的内部结构。
实例
// 不推荐:测试实现细节
expect(await page.evaluate(() => window.__store.getState().user.name))
.toBe('runoob');
// 推荐:测试用户看到的内容
await expect(page.getByText('欢迎,runoob')).toBeVisible();
expect(await page.evaluate(() => window.__store.getState().user.name))
.toBe('runoob');
// 推荐:测试用户看到的内容
await expect(page.getByText('欢迎,runoob')).toBeVisible();
测试隔离
每个测试应该完全独立于其他测试,不依赖于前一个测试的状态。
Playwright 默认提供独立 Context,但你还应该确保不依赖测试的执行顺序。
实例
// 不推荐:测试之间相互依赖
let createdId;
test('创建资源', async ({ request }) => {
const resp = await request.post('/api/items');
createdId = (await resp.json()).id; // 共享状态
});
test('使用创建的资源', async ({ page }) => {
await page.goto(`/items/${createdId}`); // 依赖上一个测试的结果
});
// 推荐:每个测试自给自足
test('创建并使用资源', async ({ request, page }) => {
const resp = await request.post('/api/items');
const id = (await resp.json()).id;
await page.goto(`/items/${id}`);
await expect(page.getByText('资源详情')).toBeVisible();
});
let createdId;
test('创建资源', async ({ request }) => {
const resp = await request.post('/api/items');
createdId = (await resp.json()).id; // 共享状态
});
test('使用创建的资源', async ({ page }) => {
await page.goto(`/items/${createdId}`); // 依赖上一个测试的结果
});
// 推荐:每个测试自给自足
test('创建并使用资源', async ({ request, page }) => {
const resp = await request.post('/api/items');
const id = (await resp.json()).id;
await page.goto(`/items/${id}`);
await expect(page.getByText('资源详情')).toBeVisible();
});
Locator 优先级原则
按照推荐的顺序使用 Locator,可以提高测试的稳定性。
| 优先级 | 方法 | 适用场景 |
|---|---|---|
| 1 | getByRole() | 元素具有明确的 ARIA 角色 |
| 2 | getByLabel() | 表单元素关联 label |
| 3 | getByPlaceholder() | 输入框有 placeholder |
| 4 | getByText() | 元素有明确文本 |
| 5 | getByAltText() | 图片有 alt 属性 |
| 6 | getByTitle() | 元素有 title 属性 |
| 7 | getByTestId() | 兜底方案 |
| 8 | locator() | CSS/XPath,最后选择 |
尽量不使用 CSS 类名作为定位器。
类名容易因样式重构而变更,导致测试大面积失效。
避免不必要的等待
利用 Playwright 的自动等待机制,避免手写 sleep 或 waitForTimeout。
实例
// 不推荐:硬编码等待
await page.waitForTimeout(3000);
await page.getByText('数据加载完成').click();
// 推荐:依赖自动等待和断言
await expect(page.getByText('数据加载中...')).toBeHidden({ timeout: 10000 });
await page.getByText('数据加载完成').click();
await page.waitForTimeout(3000);
await page.getByText('数据加载完成').click();
// 推荐:依赖自动等待和断言
await expect(page.getByText('数据加载中...')).toBeHidden({ timeout: 10000 });
await page.getByText('数据加载完成').click();
不要测试第三方服务
只测试你能控制的内容,不要依赖第三方服务的可用性。
实例
// 不推荐:依赖外部 CDN 或 API
await page.goto('https://www.runoob.com/');
// 页面可能加载了 Google Analytics、外部字体等
// 推荐:拦截第三方请求,Mock 外部服务
await page.route('**/*analytics*', route => route.abort());
await page.route('**/external-api/**', route => {
route.fulfill({ json: { status: 'ok' } });
});
await page.goto('https://www.runoob.com/');
// 页面可能加载了 Google Analytics、外部字体等
// 推荐:拦截第三方请求,Mock 外部服务
await page.route('**/*analytics*', route => route.abort());
await page.route('**/external-api/**', route => {
route.fulfill({ json: { status: 'ok' } });
});
使用 expect.soft() 软断言
在检查多个独立条件时,软断言可以一次性报告所有失败,而不是在第一个失败处停止。
实例
// 推荐:软断言一次性检测所有字段
await expect.soft(page.getByLabel('用户名')).toBeVisible();
await expect.soft(page.getByLabel('密码')).toBeVisible();
await expect.soft(page.getByLabel('邮箱')).toBeVisible();
await expect.soft(page.getByRole('button', { name: '注册' })).toBeEnabled();
// 如果多个字段缺失,一次性全部报告
await expect.soft(page.getByLabel('用户名')).toBeVisible();
await expect.soft(page.getByLabel('密码')).toBeVisible();
await expect.soft(page.getByLabel('邮箱')).toBeVisible();
await expect.soft(page.getByRole('button', { name: '注册' })).toBeEnabled();
// 如果多个字段缺失,一次性全部报告
合理组织测试文件
| 实践 | 说明 |
|---|---|
| 按功能模块分文件 | login.spec.ts、checkout.spec.ts 等 |
| 使用 describe 分组 | 将相关测试放在 test.describe() 中 |
| 用好 beforeEach | 重复的导航和准备步骤提取到 beforeEach |
| 测试名称描述性 | 好的名称能说明「做了什么」和「期望什么」 |
| 提取公共代码 | 超过 3 个文件使用的 helper 提取到共享模块 |
| 不要过度 DRY | 2-3 行重复比过度抽象更容易维护 |
常见问题排错
测试不稳定(flaky)
不稳定测试的原因和解决方案:
| 原因 | 解决方案 |
|---|---|
| 硬编码 sleep 不够长 | 使用自动等待机制替代固定等待 |
| 依赖第三方服务 | 使用 route() Mock 外部请求 |
| 动画导致元素位置变化 | 在配置中 animations: 'disabled' |
| 测试间数据残留 | 确保每个测试独立创建和清理数据 |
| 时间相关逻辑 | 使用 page.clock 固定时间 |
Locator 找不到元素
排查步骤:
- 确认元素是否在 DOM 中(用
toBeAttached()而非toBeVisible()) - 确认元素是否在视口内
- 确认是否被 iframe 嵌套
- 在 UI Mode 中使用 Pick Locator 检查定位器
- 检查是否有多个匹配元素(Locator 默认要求严格模式)
