端到端测试实战:Playwright自动化测试指南
·
端到端测试实战:Playwright自动化测试指南
前言
各位前端小伙伴,不知道你们有没有这样的经历:每次改完代码,都要手动打开浏览器点点点,测试各种功能是否正常。
如果项目复杂,这个过程简直是噩梦!
我曾经为了测试一个表单功能,连续点了半小时鼠标,手都快抽筋了。后来发现了Playwright,才知道原来测试还可以这样玩!
什么是Playwright?
Playwright是一个由Microsoft开发的端到端测试框架。它可以自动化测试浏览器行为,模拟真实用户操作。相比Cypress,Playwright有以下优势:
- 跨浏览器支持:支持Chrome、Firefox、Safari、Edge
- 多语言支持:JavaScript/TypeScript、Python、Java、C#
- 自动等待:智能等待元素出现,无需手动设置timeout
- 网络拦截:可以拦截和修改网络请求
- 移动端测试:支持模拟移动端设备
快速上手
安装依赖
npm install -D playwright
npx playwright install
编写第一个测试
创建tests/login.spec.ts:
import { test, expect } from '@playwright/test'
test('login test', async ({ page }) => {
await page.goto('https://example.com/login')
await page.fill('#username', 'testuser')
await page.fill('#password', 'testpass')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('https://example.com/dashboard')
await expect(page.locator('text=Welcome, testuser')).toBeVisible()
})
运行测试
npx playwright test
核心概念
Page对象
page是Playwright的核心对象,代表一个浏览器页面:
await page.goto(url) // 导航到URL
await page.click(selector) // 点击元素
await page.fill(selector, text) // 填写表单
await page.type(selector, text) // 模拟键盘输入
await page.waitForSelector(selector) // 等待元素出现
await page.screenshot({ path: 'screenshot.png' }) // 截图
Locator定位器
Locator是Playwright推荐的元素定位方式,支持多种选择器:
page.locator('text=Submit') // 文本选择器
page.locator('button') // 标签选择器
page.locator('#username') // ID选择器
page.locator('.btn-primary') // 类选择器
page.locator('[data-testid="submit"]') // 属性选择器
page.locator('//button[@type="submit"]') // XPath选择器
自动等待
Playwright会自动等待元素可操作,无需手动设置等待:
// Playwright会自动等待按钮可点击
await page.click('button[type="submit"]')
// 自动等待文本出现
await expect(page.locator('text=Success')).toBeVisible()
实战示例
测试登录流程
import { test, expect } from '@playwright/test'
test.describe('Login functionality', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password123')
await page.click('button:has-text("Login")')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('.user-name')).toHaveText('Welcome, User')
})
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.fill('#email', 'wrong@example.com')
await page.fill('#password', 'wrongpassword')
await page.click('button:has-text("Login")')
await expect(page.locator('.error-message')).toBeVisible()
await expect(page.locator('.error-message')).toHaveText('Invalid credentials')
})
})
测试表单验证
test('form validation', async ({ page }) => {
await page.goto('/register')
await page.click('button:has-text("Submit")')
await expect(page.locator('#email-error')).toHaveText('Email is required')
await expect(page.locator('#password-error')).toHaveText('Password is required')
await page.fill('#email', 'invalid-email')
await page.click('button:has-text("Submit")')
await expect(page.locator('#email-error')).toHaveText('Invalid email format')
})
网络拦截
test('should mock API response', async ({ page }) => {
await page.route('/api/users', async (route) => {
const json = [{ id: 1, name: 'Mock User' }]
await route.fulfill({ json })
})
await page.goto('/users')
await expect(page.locator('text=Mock User')).toBeVisible()
})
移动端测试
import { test, expect, devices } from '@playwright/test'
test.use({
...devices['iPhone 14'],
})
test('mobile login', async ({ page }) => {
await page.goto('/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password')
await page.click('button:has-text("Login")')
await expect(page).toHaveURL('/dashboard')
})
截图和录屏
test('should capture screenshot', async ({ page }) => {
await page.goto('/home')
await page.screenshot({
path: 'homepage.png',
fullPage: true
})
})
test('should record video', async ({ page }, testInfo) => {
await page.goto('/checkout')
await page.click('#buy-now')
// 测试失败时自动录屏
testInfo.video?.path()
})
高级用法
Page Object模式
// pages/LoginPage.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('#email', email)
await this.page.fill('#password', password)
await this.page.click('button:has-text("Login")')
}
async getErrorMessage() {
return this.page.locator('.error-message').textContent()
}
}
// test
test('login with page object', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password')
await expect(page).toHaveURL('/dashboard')
})
并行测试
Playwright默认支持并行测试,可以通过配置调整:
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
workers: 4, // 4个并行工作进程
testMatch: '**/*.spec.ts',
})
测试报告
# 生成HTML报告
npx playwright test --reporter=html
# 生成JSON报告
npx playwright test --reporter=json
最佳实践
1. 使用data-testid属性
<!-- ✅ 推荐 -->
<button data-testid="submit-btn">Submit</button>
<!-- ❌ 不推荐 -->
<button class="btn btn-primary">Submit</button>
await page.click('[data-testid="submit-btn"]')
2. 保持测试独立
test.describe('User management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
await page.fill('#email', 'admin@example.com')
await page.fill('#password', 'admin')
await page.click('button:has-text("Login")')
})
test('create user', async ({ page }) => {
// 测试代码
})
test('delete user', async ({ page }) => {
// 测试代码
})
})
3. 避免依赖执行顺序
// ❌ 错误:delete测试依赖create测试
test('create user', async ({ page }) => {})
test('delete user', async ({ page }) => {})
// ✅ 正确:每个测试都是独立的
test('create user', async ({ page }) => {
await createUser()
})
test('delete user', async ({ page }) => {
await createUser() // 每个测试自己创建数据
await deleteUser()
})
4. 使用fixtures
// fixtures.ts
import { test as base } from '@playwright/test'
type MyFixtures = {
authenticatedPage: Page
}
export const test = base.extend<MyFixtures>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password')
await page.click('button:has-text("Login")')
await use(page)
}
})
// 使用fixture
test('dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard')
})
常见误区
误区1:使用硬编码的等待时间
// ❌ 错误
await page.click('button')
await page.waitForTimeout(1000) // 不推荐
// ✅ 正确:Playwright会自动等待
await page.click('button')
await expect(page.locator('text=Success')).toBeVisible()
误区2:测试过多细节
// ❌ 测试实现细节
test('should call API when button is clicked', async ({ page }) => {
// 不要测试内部实现
})
// ✅ 测试用户行为
test('should show results when search button is clicked', async ({ page }) => {
await page.fill('#search', 'keyword')
await page.click('button:has-text("Search")')
await expect(page.locator('.results')).toBeVisible()
})
误区3:测试环境不稳定
确保测试环境是隔离的、可重复的:
- 使用Mock数据
- 每次测试后清理数据
- 使用测试专用的数据库
总结
Playwright是一个强大的端到端测试工具,它可以模拟真实用户操作,帮助我们发现UI层面的bug。记住:
- 测试应该模拟真实用户行为
- 使用Locator定位元素
- 利用自动等待特性
- 保持测试独立和可重复
现在,告别手动测试,让Playwright帮你完成重复性工作吧!你会发现,原来测试也可以这么轻松!
最后提醒:端到端测试虽然强大,但不要过度使用。单元测试+集成测试+少量端到端测试是最佳组合!
更多推荐
所有评论(0)