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