Playwright 最佳实践 #
测试设计原则 #
1. 测试独立性 #
每个测试应该独立运行,不依赖其他测试:
typescript
// ✅ 推荐 - 独立的测试
test('用户可以登录', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
test('用户可以查看资料', async ({ page }) => {
// 独立准备数据
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await page.goto('/profile');
await expect(page.locator('.profile')).toBeVisible();
});
// ❌ 不推荐 - 依赖其他测试
test('步骤 1: 登录', async ({ page }) => {
await page.goto('/login');
// ...
});
test('步骤 2: 查看资料', async ({ page }) => {
// 假设已经登录
await page.goto('/profile'); // 可能失败
});
2. 测试原子性 #
每个测试只验证一个功能点:
typescript
// ✅ 推荐 - 原子测试
test('用户名显示正确', async ({ page }) => {
await page.goto('/profile');
await expect(page.locator('.username')).toHaveText('John Doe');
});
test('用户邮箱显示正确', async ({ page }) => {
await page.goto('/profile');
await expect(page.locator('.email')).toHaveText('john@example.com');
});
// ❌ 不推荐 - 测试太多功能
test('用户资料页面', async ({ page }) => {
await page.goto('/profile');
await expect(page.locator('.username')).toHaveText('John Doe');
await expect(page.locator('.email')).toHaveText('john@example.com');
await expect(page.locator('.phone')).toHaveText('123-456-7890');
await expect(page.locator('.address')).toHaveText('123 Main St');
// ... 更多断言
});
3. 测试可读性 #
使用清晰的命名和结构:
typescript
// ✅ 推荐 - 清晰的命名
test('当用户输入有效凭据时应该成功登录', async ({ page }) => {
// Arrange
const email = 'user@example.com';
const password = 'validPassword123';
// Act
await page.goto('/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign In' }).click();
// Assert
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
// ❌ 不推荐 - 模糊的命名
test('test1', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password');
await page.click('button');
});
元素定位最佳实践 #
1. 优先使用用户可见的定位器 #
typescript
// ✅ 推荐 - 用户可见的定位器
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('test@example.com');
await page.getByPlaceholder('Enter your name').fill('John');
await page.getByText('Welcome').click();
// ❌ 不推荐 - 依赖实现细节
await page.locator('#submit-btn').click();
await page.locator('.email-input').fill('test@example.com');
await page.locator('[data-testid="name"]').fill('John');
2. 使用语义化选择器 #
typescript
// ✅ 推荐 - 语义化
await page.getByRole('navigation').getByRole('link', { name: 'Products' }).click();
await page.getByRole('article').filter({ hasText: 'News' }).click();
// ❌ 不推荐 - CSS 类名
await page.locator('.nav > ul > li:nth-child(2) > a').click();
await page.locator('.article.news').click();
3. 避免使用 XPath #
typescript
// ✅ 推荐
await page.getByRole('button', { name: 'Submit' }).click();
// ❌ 不推荐
await page.locator('//button[@type="submit"]').click();
等待策略 #
1. 使用自动等待 #
typescript
// ✅ 推荐 - 依赖自动等待
await page.click('button');
await expect(page.locator('.result')).toBeVisible();
// ❌ 不推荐 - 固定等待
await page.click('button');
await page.waitForTimeout(2000);
2. 使用断言等待 #
typescript
// ✅ 推荐 - 使用断言
await expect(page.locator('.loading')).not.toBeVisible();
await expect(page.locator('.content')).toBeVisible();
// ❌ 不推荐 - 手动等待
await page.waitForSelector('.loading', { state: 'hidden' });
await page.waitForSelector('.content', { state: 'visible' });
3. 网络等待 #
typescript
// ✅ 推荐 - 等待特定请求
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button:has-text("Load")');
const response = await responsePromise;
// ❌ 不推荐 - 等待网络空闲
await page.waitForLoadState('networkidle');
代码组织 #
1. 使用 Page Object 模式 #
typescript
// ✅ 推荐 - Page Object
class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign In' }).click();
}
}
test('登录测试', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
2. 使用测试夹具 #
typescript
// ✅ 推荐 - 复用通用设置
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await use(page);
},
});
test('需要认证的测试', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
await expect(authenticatedPage.locator('.welcome')).toBeVisible();
});
3. 合理分组 #
typescript
// ✅ 推荐 - 按功能分组
test.describe('用户认证', () => {
test.describe('登录', () => {
test('使用有效凭据登录', async ({ page }) => {});
test('使用无效凭据登录失败', async ({ page }) => {});
});
test.describe('注册', () => {
test('新用户注册', async ({ page }) => {});
test('已存在邮箱注册失败', async ({ page }) => {});
});
});
性能优化 #
1. 并行执行 #
typescript
// playwright.config.ts
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 1 : '50%',
});
2. 测试分片 #
bash
# CI 中分片执行
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4
3. 只运行变更的测试 #
bash
# 只运行与变更文件相关的测试
npx playwright test --only-changed
4. 优化浏览器启动 #
typescript
// playwright.config.ts
export default defineConfig({
use: {
// 忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 禁用图片加载
// ...
},
});
测试数据管理 #
1. 使用 Mock 数据 #
typescript
// ✅ 推荐 - 使用 Mock
test.beforeEach(async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 1, name: 'Test User 1' },
{ id: 2, name: 'Test User 2' },
]),
});
});
});
2. 清理测试数据 #
typescript
// ✅ 推荐 - 清理数据
test.afterEach(async ({ request }) => {
// 删除测试创建的数据
const response = await request.get('/api/users?email=test@example.com');
const users = await response.json();
for (const user of users) {
await request.delete(`/api/users/${user.id}`);
}
});
3. 使用环境变量 #
typescript
// ✅ 推荐 - 使用环境变量
test('使用环境变量', async ({ page }) => {
const username = process.env.TEST_USERNAME!;
const password = process.env.TEST_PASSWORD!;
await page.fill('#username', username);
await page.fill('#password', password);
});
错误处理 #
1. 有意义的错误消息 #
typescript
// ✅ 推荐 - 添加错误消息
await expect(
page.locator('.error-message'),
'登录失败时应显示错误消息'
).toBeVisible();
// ❌ 不推荐 - 没有上下文
await expect(page.locator('.error-message')).toBeVisible();
2. 软断言 #
typescript
// ✅ 推荐 - 收集所有错误
test('验证页面元素', async ({ page }) => {
await expect.soft(page.locator('.header')).toBeVisible();
await expect.soft(page.locator('.footer')).toBeVisible();
await expect.soft(page.locator('.sidebar')).toBeVisible();
});
3. 条件跳过 #
typescript
// ✅ 推荐 - 条件跳过
test('Safari 特定功能', async ({ page, browserName }) => {
test.skip(browserName !== 'webkit', '仅在 Safari 上运行');
// 测试代码
});
调试技巧 #
1. 使用 page.pause() #
typescript
test('调试测试', async ({ page }) => {
await page.goto('/');
await page.pause(); // 暂停执行
await page.click('button');
});
2. 失败时截图 #
typescript
// playwright.config.ts
export default defineConfig({
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry',
},
});
3. 控制台日志 #
typescript
test('监听控制台', async ({ page }) => {
page.on('console', msg => {
console.log(`浏览器: ${msg.text()}`);
});
page.on('pageerror', error => {
console.error(`页面错误: ${error.message}`);
});
await page.goto('/');
});
安全考虑 #
1. 不要硬编码凭据 #
typescript
// ✅ 推荐 - 使用环境变量
const username = process.env.TEST_USERNAME;
const password = process.env.TEST_PASSWORD;
// ❌ 不推荐 - 硬编码凭据
const username = 'admin';
const password = 'password123';
2. 不要提交敏感数据 #
gitignore
# .gitignore
.env
.env.local
auth.json
*.pem
3. 使用 secrets 管理 #
yaml
# GitHub Actions
env:
TEST_USERNAME: ${{ secrets.TEST_USERNAME }}
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
测试报告 #
1. 使用多种报告器 #
typescript
// playwright.config.ts
export default defineConfig({
reporter: [
['list'],
['html', { outputFolder: 'playwright-report' }],
['junit', { outputFile: 'junit.xml' }],
],
});
2. 添加测试元数据 #
typescript
test('重要功能', {
tag: '@smoke',
annotation: {
type: 'issue',
description: 'https://github.com/repo/issues/123',
},
}, async ({ page }) => {
// 测试代码
});
持续改进 #
1. 定期审查测试 #
- 移除过时的测试
- 更新测试数据
- 优化慢速测试
2. 监控测试性能 #
bash
# 查看测试执行时间
npx playwright test --reporter=list
3. 保持测试简洁 #
typescript
// ✅ 推荐 - 简洁的测试
test('用户可以登录', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL('/dashboard');
});
下一步 #
现在你已经掌握了最佳实践,接下来学习 故障排查 了解如何解决常见问题!
最后更新:2026-03-28