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 | ❌ 各浏览器不同 | ✅ 统一 |
| 网络拦截 | ✅ 内置 | ❌ 需要额外工具 | ✅ 内置 |
| 移动测试 | ✅ 支持 | ❌ 复杂 | ❌ 不支持 |
核心优势
- 快速可靠 - 自动等待机制减少不稳定的测试
- 强大的选择器 - 支持文本、CSS、XPath 等多种选择器
- 并行执行 - 默认并行运行测试用例
- 丰富的断言 - 内置多种断言方法
- 调试友好 - 提供 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 和良好的生态
🚀 使用建议
- 从简单开始 - 先写基础的页面加载测试
- 逐步完善 - 添加交互、表单、网络等测试
- 重视维护 - 使用 POM 模式组织代码
- 持续优化 - 监控测试稳定性和执行时间
📈 未来发展
- 更好的组件测试支持
- AI 辅助的测试生成
- 更丰富的性能分析功能
- 与开发工具的深度集成
Playwright 让端到端测试变得更加简单和可靠,是现代 Web 开发不可或缺的工具。开始使用 Playwright,让你的应用质量更上一层楼!
资源链接
自动化测试是软件质量的保障,Playwright 让这个过程更加高效和愉悦! 🎭