Playwright:现代 Web 应用的端到端测试框架
任何浏览器 · 任何平台 · 一个 API
🦢 什么是 Playwright?
Playwright 是 Microsoft 开源的端到端(E2E)测试框架,专为现代 Web 应用设计。它提供了可靠的自动化测试能力,支持所有主流浏览器和平台,无需妥协。
核心特点
| 特点 | 说明 |
|---|---|
| 跨浏览器 | 支持 Chromium、WebKit、Firefox 所有现代渲染引擎 |
| 跨平台 | Windows、Linux、macOS 本地运行,支持 CI/CD |
| 跨语言 | TypeScript、JavaScript、Python、.NET、Java |
| 移动模拟 | 原生模拟 Chrome for Android 和 Mobile Safari |
| 无闪测 | 自动等待 + Web 优先断言,消除不稳定测试 |
| 完整隔离 | 每个测试独立浏览器上下文,零开销隔离 |
🎯 为什么选择 Playwright?
传统测试框架的痛点
❌ 需要手动等待元素加载 ❌ 测试之间相互污染 ❌ 不同浏览器行为不一致 ❌ 调试困难,失败后难以复现 ❌ 无法模拟真实用户交互
Playwright 的解决方案
✅ 自动等待元素可操作后再执行 ✅ 每个测试独立浏览器上下文(类似新浏览器配置文件) ✅ 统一 API,所有浏览器行为一致 ✅ 内置 Trace Viewer、录屏、截图、DOM 快照 ✅ 使用真实浏览器输入管道,事件完全可信
🚀 快速开始
系统要求
- Node.js: 20.x / 22.x / 24.x(最新版)
- Windows: Windows 11+ / Windows Server 2019+ / WSL
- macOS: macOS 14 (Ventura) 或更高
- Linux: Debian 12/13, Ubuntu 22.04/24.04 (x86-64 或 arm64)
安装 Playwright
# 使用 npm 初始化新项目 npm init playwright@latest # 或使用 yarn yarn create playwright # 或使用 pnpm pnpm create playwright
安装过程中的选择
1. 选择语言:TypeScript 或 JavaScript(默认 TypeScript) 2. 测试文件夹名称:tests 或 e2e(默认 tests) 3. 是否添加 GitHub Actions 工作流(推荐用于 CI) 4. 是否安装 Playwright 浏览器(默认 yes)
项目结构
安装完成后,项目结构如下:
my-project/
├── playwright.config.ts # 测试配置
├── package.json
├── package-lock.json # 或 yarn.lock / pnpm-lock.yaml
└── tests/
└── example.spec.ts # 最小示例测试
运行测试
# 运行所有测试(默认 headless 模式,并行执行) npx playwright test # 在浏览器窗口中运行(有头模式) npx playwright test --headed # 只运行 Chromium 浏览器 npx playwright test --project=chromium # 运行单个测试文件 npx playwright test tests/example.spec.ts # 打开 UI 模式(推荐用于调试) npx playwright test --ui
查看测试报告
# 测试完成后自动打开 HTML 报告(仅当有失败时) # 或手动打开 npx playwright show-report
📝 第一个测试
基本示例
import { test, expect } from '@playwright/test';
test('首页应该显示正确的标题', async ({ page }) => {
await page.goto('https://example.com');
// 等待并检查页面标题
await expect(page).toHaveTitle(/Example Domain/);
// 检查页面内容
await expect(page.locator('h1')).toContainText('Example Domain');
});
多浏览器测试
import { test, expect } from '@playwright/test';
test.describe('跨浏览器测试', () => {
test('应该在所有浏览器中正常工作', async ({ page }) => {
await page.goto('https://playwright.dev');
// Playwright 会自动等待元素可操作
await page.click('text=Get Started');
// 断言会自动重试直到条件满足
await expect(page.locator('h1')).toBeVisible();
});
});
移动设备模拟
import { test, devices } from '@playwright/test';
test('在 iPhone 上测试', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 13']
});
const page = await context.newPage();
await page.goto('https://example.com');
// 测试移动端布局...
await context.close();
});
🔧 核心 API
浏览器类型
import { chromium, firefox, webkit } from 'playwright';
// 启动 Chromium
const browser = await chromium.launch();
// 启动 Firefox
const browser = await firefox.launch();
// 启动 WebKit (Safari 引擎)
const browser = await webkit.launch();
页面操作
// 导航
await page.goto('https://example.com');
await page.goBack();
await page.goForward();
await page.reload();
// 点击
await page.click('button');
await page.dblclick('button');
await page.hover('button');
// 输入
await page.fill('input[name="email"]', 'test@example.com');
await page.type('input[name="password"]', 'secret');
await page.press('input[name="password"]', 'Enter');
// 选择
await page.selectOption('select#country', 'US');
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
定位器(Locators)
Playwright 推荐使用用户友好的定位器:
// 按文本
page.getByText('Welcome');
page.getByRole('button', { name: 'Submit' });
page.getByLabel('Email');
page.getByPlaceholder('Enter your name');
page.getByAltText('Logo');
page.getByTitle('Help');
// 按测试 ID(推荐)
page.getByTestId('submit-button');
// CSS 选择器
page.locator('.container > .item');
// XPath
page.locator('//button[text()="Submit"]');
断言(Assertions)
import { expect } from '@playwright/test';
// 可见性
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeAttached();
// 文本内容
await expect(locator).toContainText('partial');
await expect(locator).toHaveText('exact match');
// 属性
await expect(locator).toHaveAttribute('type', 'submit');
await expect(locator).toHaveClass('active');
await expect(locator).toHaveValue('input value');
// 计数
await expect(locator).toHaveCount(3);
// URL
await expect(page).toHaveURL(/.*dashboard/);
// 截图对比
await expect(page).toHaveScreenshot();
🛠️ 强大工具集
1. Codegen — 录制测试
通过记录操作自动生成测试代码:
# 打开浏览器开始录制 npx playwright codegen # 指定目标 URL npx playwright codegen https://example.com # 指定输出语言 npx playwright codegen --lang=python https://example.com
支持语言:TypeScript、JavaScript、Python、Java、C#
2. Playwright Inspector — 调试器
逐步执行测试,查看点击位置,探索执行日志:
# 调试模式运行 npx playwright test --debug # 或设置环境变量 PWDEBUG=1 npx playwright test
功能:
- 逐步执行测试
- 查看实时 DOM 快照
- 生成选择器
- 检查点击位置
- 查看执行日志
3. Trace Viewer — 追踪查看器
捕获测试失败的完整信息:
# 在配置中启用追踪
// playwright.config.ts
export default {
use: {
trace: 'on-first-retry' // 首次重试时记录追踪
}
};
# 查看追踪文件
npx playwright show-trace trace.zip
追踪内容包括:
- 测试执行录屏
- 实时 DOM 快照
- 操作探索器
- 测试源码
- 网络请求
- 控制台日志
- 错误堆栈
4. UI Mode — UI 测试模式
实时观察测试执行,支持时间旅行调试:
npx playwright test --ui
功能:
- 监视模式(文件变更自动重跑)
- 实时步骤视图
- 时间旅行调试
- 追踪集成
- 按状态/浏览器过滤
📦 高级功能
浏览器上下文(Browser Contexts)
每个测试获得完全隔离的浏览器上下文:
import { test } from '@playwright/test';
test('测试隔离', async ({ browser }) => {
// 创建独立上下文(类似新的浏览器配置文件)
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// 两个页面完全隔离:不同的 Cookie、LocalStorage 等
await page1.goto('https://example.com');
await page2.goto('https://example.com');
await context1.close();
await context2.close();
});
优势:
- 完全测试隔离
- 零开销(创建仅需几毫秒)
- 无需清理状态
登录一次,多次使用
保存认证状态并在所有测试中重用:
// playwright.config.ts
export default {
use: {
storageState: 'auth.json' // 使用保存的认证状态
}
};
// 保存认证状态
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForURL('https://example.com/dashboard');
// 保存认证状态
await page.context().storageState({ path: 'auth.json' });
});
多标签、多用户测试
import { test, expect } from '@playwright/test';
test('多用户协作测试', async ({ browser }) => {
// 创建两个独立用户上下文
const user1Context = await browser.newContext();
const user2Context = await browser.newContext();
const user1Page = await user1Context.newPage();
const user2Page = await user2Context.newPage();
// 用户 1 创建文档
await user1Page.goto('https://example.com/docs/new');
await user1Page.fill('input[name="title"]', 'My Doc');
await user1Page.click('button:has-text("Create")');
// 用户 2 查看并编辑
await user2Page.goto('https://example.com/docs');
await user2Page.click('text=My Doc');
await user2Page.fill('textarea', 'Adding content...');
// 验证两个用户看到相同内容
await expect(user1Page.locator('textarea')).toHaveValue('Adding content...');
await user1Context.close();
await user2Context.close();
});
API 测试
Playwright 也支持纯 API 测试:
import { test, expect } from '@playwright/test';
test('API 测试', async ({ request }) => {
// 发送 POST 请求创建资源
const createResponse = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@example.com'
}
});
expect(createResponse.ok()).toBeTruthy();
const user = await createResponse.json();
// 验证资源已创建
const getResponse = await request.get(`/api/users/${user.id}`);
const userData = await getResponse.json();
expect(userData.name).toBe('John Doe');
});
网络拦截与 Mock
import { test } from '@playwright/test';
test('网络拦截', async ({ page }) => {
// Mock API 响应
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Mocked User' }])
});
});
// Mock 失败场景
await page.route('**/api/error', route => {
route.fulfill({
status: 500,
body: 'Internal Server Error'
});
});
await page.goto('https://example.com');
});
文件上传下载
import { test, expect } from '@playwright/test';
import path from 'path';
test('文件上传', async ({ page }) => {
await page.goto('https://example.com/upload');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.join(__dirname, 'test-file.pdf'));
await page.click('button:has-text("Upload")');
await expect(page.locator('.success')).toBeVisible();
});
test('文件下载', async ({ page }) => {
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('a:has-text("Download")')
]);
// 保存下载的文件
await download.saveAs('./downloads/my-file.pdf');
// 或获取下载路径
const downloadPath = download.path();
});
📊 测试配置
完整配置示例
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// 测试文件位置
testDir: './tests',
// 超时设置
timeout: 30 * 1000,
expect: {
timeout: 5000
},
// 失败重试
retries: 2,
// 并行执行
workers: 4,
// 报告器
reporter: [
['html'],
['json', { outputFile: 'test-results.json' }],
['junit', { outputFile: 'junit.xml' }]
],
// 共享配置
use: {
// 基础 URL
baseURL: 'https://example.com',
// 追踪设置
trace: 'on-first-retry',
// 录屏
video: 'retain-on-failure',
// 截图
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'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] }
}
]
});
🔄 CI/CD 集成
GitHub Actions
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- 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@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Docker 运行
FROM mcr.microsoft.com/playwright:v1.40.0-jammy WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . CMD ["npx", "playwright", "test"]
🎯 最佳实践
1. 使用用户友好的定位器
// ✅ 推荐
page.getByRole('button', { name: 'Submit' });
page.getByLabel('Email');
page.getByTestId('submit-btn');
// ❌ 避免
page.locator('div > div:nth-child(2) > button.blue');
page.locator('//button[@class="btn btn-primary"]');
2. 避免硬编码等待
// ❌ 避免
await page.waitForTimeout(5000);
// ✅ 推荐
await page.waitForSelector('.loaded');
await expect(page.locator('h1')).toBeVisible();
3. 使用 Page Object 模式
// tests/pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('input[name="email"]', email);
await this.page.fill('input[name="password"]', password);
await this.page.click('button[type="submit"]');
}
async isLoggedIn() {
return this.page.url().includes('/dashboard');
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';
test('登录成功', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(loginPage.isLoggedIn()).toBeTruthy();
});
4. 使用 Fixtures 共享设置
// tests/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
export const test = base.extend<{
authenticatedPage: Page;
}>({
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
// 登录
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
await use(page);
await context.close();
}
});
// tests/dashboard.spec.ts
import { test, expect } from './fixtures/auth.fixture';
test('仪表盘应该显示欢迎信息', async ({ authenticatedPage }) => {
await expect(authenticatedPage.locator('h1'))
.toContainText('Welcome');
});
📚 学习资源
| 资源 | 链接 |
|---|---|
| 官方文档 | https://playwright.dev |
| Python 文档 | https://playwright.dev/python |
| .NET 文档 | https://playwright.dev/dotnet |
| Java 文档 | https://playwright.dev/java |
| GitHub 仓库 | https://github.com/microsoft/playwright |
| Discord 社区 | https://aka.ms/playwright/discord |
| Stack Overflow | https://stackoverflow.com/questions/tagged/playwright |
🦢 总结
Playwright 是目前最强大的 Web 自动化测试工具之一,它解决了传统测试框架的诸多痛点:
核心优势
| 优势 | 说明 |
|---|---|
| 可靠性 | 自动等待 + 智能断言,消除闪测 |
| 速度 | 并行执行 + 轻量上下文,快速反馈 |
| 调试 | Trace Viewer + UI Mode + Inspector,问题一目了然 |
| 覆盖 | 所有主流浏览器 + 移动设备模拟 |
| 生态 | 多语言支持 + 活跃社区 + 持续更新 |
适用场景
- ✅ Web 应用端到端测试
- ✅ 跨浏览器兼容性测试
- ✅ 移动端 Web 测试
- ✅ API 测试
- ✅ 视觉回归测试
- ✅ 性能监控
- ✅ 自动化脚本
不适合
- ❌ 桌面应用测试(考虑 PyAutoGUI)
- ❌ 原生移动应用测试(考虑 Appium)
- ❌ 游戏测试(考虑专用游戏测试框架)
可靠的测试不是奢侈品,而是现代 Web 开发的必需品。