Playwright 端到端测试完全指南

深入了解如何使用 Playwright 进行现代 Web 应用的端到端测试,包括最佳实践和高级用法。

6 分钟阅读
Talljack
1200
·--
Playwright测试E2EWeb开发自动化

Playwright 端到端测试完全指南

Playwright 是微软开源的现代端到端测试框架,支持多浏览器、多平台的自动化测试。在这篇文章中,我将全面介绍 Playwright 的使用方法和最佳实践。

什么是 Playwright?

Playwright 是一个用于 Web 应用自动化测试的开源框架,由微软开发。它支持:

  • 多浏览器: Chromium、Firefox、WebKit (Safari)
  • 跨平台: Windows、Linux、macOS
  • 多语言: JavaScript/TypeScript、Python、Java、C#
  • 现代特性: 自动等待、拦截网络请求、移动设备模拟

为什么选择 Playwright?

与其他测试工具对比

特性 Playwright Selenium Cypress
多浏览器支持 ✅ 原生支持 ✅ 需要驱动 ❌ 主要 Chrome
速度 🚀 极快 🐌 较慢 ⚡ 快
API 一致性 ✅ 统一 API ❌ 各浏览器不同 ✅ 统一
网络拦截 ✅ 内置 ❌ 需要额外工具 ✅ 内置
移动测试 ✅ 支持 ❌ 复杂 ❌ 不支持

核心优势

  1. 快速可靠 - 自动等待机制减少不稳定的测试
  2. 强大的选择器 - 支持文本、CSS、XPath 等多种选择器
  3. 并行执行 - 默认并行运行测试用例
  4. 丰富的断言 - 内置多种断言方法
  5. 调试友好 - 提供 UI 模式和跟踪功能

快速开始

安装 Playwright

# 创建新项目
npm init playwright@latest

# 或在已有项目中安装
npm install -D @playwright/test

# 安装浏览器
npx playwright install

基本配置

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // 移动端测试
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

基础测试编写

第一个测试

// tests/example.spec.ts
import { test, expect } from '@playwright/test'

test('博客首页加载正常', async ({ page }) => {
  await page.goto('/')

  // 检查页面标题
  await expect(page).toHaveTitle(/我的博客/)

  // 检查主标题存在
  await expect(page.locator('h1')).toContainText('欢迎来到')

  // 检查导航链接
  await expect(page.locator('nav a[href="/blog"]')).toBeVisible()
})

test('博客列表页功能', async ({ page }) => {
  await page.goto('/blog')

  // 检查页面标题
  await expect(page.locator('h1')).toContainText('博客')

  // 检查文章卡片存在
  const articleCards = page.locator('article')
  await expect(articleCards).toHaveCountGreaterThan(0)

  // 点击第一篇文章
  await articleCards.first().locator('a').first().click()

  // 验证跳转到文章详情页
  await expect(page.url()).toMatch(/\/blog\/[^\/]+$/)
})

高级选择器

test('选择器示例', async ({ page }) => {
  await page.goto('/blog')

  // CSS 选择器
  await page.locator('.blog-card').first().click()

  // 文本选择器
  await page.locator('text=阅读更多').click()

  // 角色选择器
  await page.locator('role=button[name="搜索"]').click()

  // 组合选择器
  await page.locator('article:has-text("Playwright")').click()

  // XPath 选择器
  await page.locator('//button[contains(text(), "提交")]').click()
})

实际应用场景

表单测试

test('评论表单提交', async ({ page }) => {
  await page.goto('/blog/hello-world')

  // 滚动到评论区
  await page.locator('#comments').scrollIntoViewIfNeeded()

  // 填写表单
  await page.fill('[name="name"]', '测试用户')
  await page.fill('[name="email"]', 'test@example.com')
  await page.fill('[name="comment"]', '这是一条测试评论')

  // 提交表单
  await page.click('button[type="submit"]')

  // 验证提交成功
  await expect(page.locator('.success-message')).toBeVisible()
  await expect(page.locator('.comment')).toContainText('测试用户')
})

网络请求拦截

test('API 请求拦截', async ({ page }) => {
  // 拦截 API 请求
  await page.route('/api/posts', async route => {
    const response = await route.fetch()
    const json = await response.json()

    // 修改响应数据
    json.posts.push({
      id: 999,
      title: '测试文章',
      content: '这是测试内容',
    })

    await route.fulfill({
      response,
      json,
    })
  })

  await page.goto('/blog')

  // 验证修改后的数据
  await expect(page.locator('text=测试文章')).toBeVisible()
})

文件上传测试

test('头像上传功能', async ({ page }) => {
  await page.goto('/profile')

  // 选择文件
  const fileChooserPromise = page.waitForEvent('filechooser')
  await page.locator('input[type="file"]').click()
  const fileChooser = await fileChooserPromise
  await fileChooser.setFiles('./tests/fixtures/avatar.png')

  // 验证上传成功
  await expect(page.locator('.avatar img')).toHaveAttribute(
    'src',
    /avatar\.png$/
  )
})

进阶技巧

页面对象模式 (POM)

// pages/BlogPage.ts
export class BlogPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/blog')
  }

  async searchPosts(query: string) {
    await this.page.fill('[placeholder="搜索文章..."]', query)
    await this.page.press('[placeholder="搜索文章..."]', 'Enter')
  }

  async clickFirstPost() {
    await this.page.locator('article').first().locator('a').first().click()
  }

  async getPostTitles() {
    return await this.page.locator('article h2').allTextContents()
  }
}

// 使用 POM
test('使用页面对象', async ({ page }) => {
  const blogPage = new BlogPage(page)

  await blogPage.goto()
  await blogPage.searchPosts('Playwright')

  const titles = await blogPage.getPostTitles()
  expect(titles).toContain('Playwright 端到端测试完全指南')
})

数据驱动测试

// 测试数据
const searchQueries = [
  { query: 'TypeScript', expectedCount: 1 },
  { query: 'Next.js', expectedCount: 2 },
  { query: 'React', expectedCount: 3 },
]

searchQueries.forEach(({ query, expectedCount }) => {
  test(`搜索 "${query}" 应该返回 ${expectedCount} 个结果`, async ({ page }) => {
    await page.goto('/blog')
    await page.fill('[placeholder="搜索文章..."]', query)
    await page.press('[placeholder="搜索文章..."]', 'Enter')

    const results = page.locator('article')
    await expect(results).toHaveCount(expectedCount)
  })
})

视觉回归测试

test('页面视觉一致性', async ({ page }) => {
  await page.goto('/blog')

  // 全页面截图对比
  await expect(page).toHaveScreenshot('blog-page.png')

  // 组件级截图对比
  await expect(page.locator('.header')).toHaveScreenshot('header.png')
})

最佳实践

1. 测试组织

test.describe('博客功能测试', () => {
  test.beforeEach(async ({ page }) => {
    // 每个测试前的准备工作
    await page.goto('/')
  })

  test.describe('文章管理', () => {
    test('创建文章', async ({ page }) => {
      // 测试逻辑
    })

    test('编辑文章', async ({ page }) => {
      // 测试逻辑
    })

    test('删除文章', async ({ page }) => {
      // 测试逻辑
    })
  })
})

2. 等待策略

test('正确的等待方式', async ({ page }) => {
  await page.goto('/blog')

  // ✅ 正确:等待元素出现
  await expect(page.locator('.loading')).toBeHidden()
  await expect(page.locator('article')).toBeVisible()

  // ❌ 错误:硬编码等待时间
  // await page.waitForTimeout(3000)

  // ✅ 正确:等待网络请求完成
  await page.waitForResponse(
    response =>
      response.url().includes('/api/posts') && response.status() === 200
  )
})

3. 错误处理

test('错误场景处理', async ({ page }) => {
  // 监听控制台错误
  page.on('console', msg => {
    if (msg.type() === 'error') {
      console.log('页面错误:', msg.text())
    }
  })

  // 监听网络失败
  page.on('requestfailed', request => {
    console.log('网络请求失败:', request.url())
  })

  await page.goto('/blog')
})

CI/CD 集成

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: npx playwright test
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

调试技巧

1. UI 模式

# 以 UI 模式运行测试
npx playwright test --ui

2. 调试模式

# 调试特定测试
npx playwright test --debug example.spec.ts

3. 录制测试

# 录制新测试
npx playwright codegen localhost:3000

性能监控

test('页面性能检查', async ({ page }) => {
  await page.goto('/blog')

  // 获取性能指标
  const performanceEntries = await page.evaluate(() => {
    return JSON.stringify(performance.getEntriesByType('navigation'))
  })

  const navigation = JSON.parse(performanceEntries)[0]

  // 断言性能指标
  expect(navigation.loadEventEnd - navigation.loadEventStart).toBeLessThan(2000)
  expect(
    navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart
  ).toBeLessThan(1000)
})

总结

Playwright 是一个功能强大、现代化的端到端测试框架,具有以下优势:

🎯 核心价值

  • 可靠性高 - 自动等待机制减少不稳定测试
  • 覆盖面广 - 支持多浏览器和移动端测试
  • 开发体验好 - 丰富的调试工具和文档
  • 维护成本低 - 统一的 API 和良好的生态

🚀 使用建议

  1. 从简单开始 - 先写基础的页面加载测试
  2. 逐步完善 - 添加交互、表单、网络等测试
  3. 重视维护 - 使用 POM 模式组织代码
  4. 持续优化 - 监控测试稳定性和执行时间

📈 未来发展

  • 更好的组件测试支持
  • AI 辅助的测试生成
  • 更丰富的性能分析功能
  • 与开发工具的深度集成

Playwright 让端到端测试变得更加简单和可靠,是现代 Web 开发不可或缺的工具。开始使用 Playwright,让你的应用质量更上一层楼!

资源链接


自动化测试是软件质量的保障,Playwright 让这个过程更加高效和愉悦! 🎭

发布于 · 作者:Talljack

更多博客
感谢阅读

评论

在GitHub讨论

点击“加载评论”来查看和参与讨论

评论将延迟加载以提升页面性能

评论由 Giscus 提供支持
需要 GitHub 账号参与讨论