Vue 3 与 Playwright 高级 E2E 测试
概述
Playwright 是一款现代化的端到端(E2E)测试框架,由 Microsoft 开发,支持 Chromium、Firefox 和 WebKit 浏览器。它提供了自动等待、网络拦截、截图对比、多标签页测试等高级功能,与 Vue 3 应用完美兼容。本集将深入探讨 Playwright 的高级特性和最佳实践,帮助你构建全面、可靠的 E2E 测试套件。
核心知识点
1. Playwright 基础配置
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e', // 测试文件目录
timeout: 30 * 1000, // 单个测试超时时间
expect: {
timeout: 5000 // 断言超时时间
},
fullyParallel: true, // 并行运行测试
forbidOnly: !!process.env.CI, // CI 环境禁止单独运行测试
retries: process.env.CI ? 2 : 0, // CI 环境重试次数
workers: process.env.CI ? 1 : undefined, // CI 环境工作进程数
reporter: [['html', { outputFolder: 'playwright-report' }]], // 测试报告格式
use: {
baseURL: 'http://localhost:5173', // 测试基础 URL
trace: 'on-first-retry', // 重试时记录追踪
screenshot: 'only-on-failure', // 失败时截图
video: 'retain-on-failure', // 失败时保留视频
headless: process.env.CI ? true : false // CI 环境无头模式
},
// 配置不同浏览器
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
// 开发服务器配置
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI
}
})2. 测试用例结构
// tests/e2e/example.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Example Tests', () => {
test.beforeEach(async ({ page }) => {
// 每个测试前访问首页
await page.goto('/')
})
test('has title', async ({ page }) => {
// 断言页面标题
await expect(page).toHaveTitle(/Vue 3 App/)
})
test('get started link', async ({ page }) => {
// 点击链接
await page.getByRole('link', { name: 'Get Started' }).click()
// 断言新页面 URL
await expect(page).toHaveURL(/.*guide/)
})
})3. Vue 组件交互
// tests/e2e/counter.spec.ts
import { test, expect } from '@playwright/test'
test('counter increments when button is clicked', async ({ page }) => {
// 访问计数器页面
await page.goto('/counter')
// 断言初始值
await expect(page.getByText('Count: 0')).toBeVisible()
// 点击增加按钮
await page.getByRole('button', { name: 'Increment' }).click()
// 断言更新后的值
await expect(page.getByText('Count: 1')).toBeVisible()
// 点击减少按钮
await page.getByRole('button', { name: 'Decrement' }).click()
// 断言最终值
await expect(page.getByText('Count: 0')).toBeVisible()
})4. 表单测试
// tests/e2e/form.spec.ts
import { test, expect } from '@playwright/test'
test('form submission works correctly', async ({ page }) => {
// 访问表单页面
await page.goto('/form')
// 填写表单
await page.getByLabel('Name').fill('John Doe')
await page.getByLabel('Email').fill('john@example.com')
await page.getByLabel('Message').fill('Hello Playwright!')
// 提交表单
await page.getByRole('button', { name: 'Submit' }).click()
// 断言成功消息
await expect(page.getByText('Form submitted successfully!')).toBeVisible()
// 断言表单重置
await expect(page.getByLabel('Name')).toBeEmpty()
await expect(page.getByLabel('Email')).toBeEmpty()
await expect(page.getByLabel('Message')).toBeEmpty()
})5. 异步操作测试
// tests/e2e/async.spec.ts
import { test, expect } from '@playwright/test'
test('async data loads correctly', async ({ page }) => {
// 访问异步数据页面
await page.goto('/async')
// 断言加载状态
await expect(page.getByText('Loading...')).toBeVisible()
// 等待数据加载完成
await expect(page.getByText('Async Data Loaded')).toBeVisible()
// 断言数据内容
await expect(page.getByRole('list')).toContainText('Item 1')
await expect(page.getByRole('list')).toContainText('Item 2')
await expect(page.getByRole('list')).toContainText('Item 3')
})6. 网络拦截
// tests/e2e/mocked-api.spec.ts
import { test, expect } from '@playwright/test'
test('mocks API response', async ({ page }) => {
// 拦截 API 请求
await page.route('**/api/data', async (route) => {
// 返回模拟数据
await route.fulfill({
status: 200,
body: JSON.stringify({
data: ['Mocked Item 1', 'Mocked Item 2', 'Mocked Item 3']
})
})
})
// 访问页面
await page.goto('/async')
// 断言模拟数据
await expect(page.getByRole('list')).toContainText('Mocked Item 1')
await expect(page.getByRole('list')).toContainText('Mocked Item 2')
await expect(page.getByRole('list')).toContainText('Mocked Item 3')
})7. 多标签页测试
// tests/e2e/multiple-tabs.spec.ts
import { test, expect } from '@playwright/test'
test('multiple tabs navigation', async ({ browser }) => {
// 创建两个页面
const context = await browser.newContext()
const page1 = await context.newPage()
const page2 = await context.newPage()
// 页面 1 访问首页
await page1.goto('/')
await expect(page1).toHaveTitle(/Vue 3 App/)
// 页面 2 访问关于页
await page2.goto('/about')
await expect(page2).toHaveURL(/.*about/)
// 关闭页面
await page1.close()
await page2.close()
})8. 文件上传测试
// tests/e2e/file-upload.spec.ts
import { test, expect } from '@playwright/test'
import path from 'path'
test('file upload works correctly', async ({ page }) => {
// 访问文件上传页面
await page.goto('/upload')
// 获取文件路径
const filePath = path.join(__dirname, 'test-file.txt')
// 上传文件
await page.getByLabel('File').setInputFiles(filePath)
// 提交表单
await page.getByRole('button', { name: 'Upload' }).click()
// 断言上传成功
await expect(page.getByText('File uploaded successfully!')).toBeVisible()
await expect(page.getByText('test-file.txt')).toBeVisible()
})9. 截图对比测试
// tests/e2e/visual-regression.spec.ts
import { test, expect } from '@playwright/test'
test('visual regression test', async ({ page }) => {
// 访问页面
await page.goto('/')
// 匹配完整页面截图
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixelRatio: 0.1 // 允许 10% 的像素差异
})
// 匹配特定元素截图
await expect(page.getByRole('header')).toHaveScreenshot('header.png')
})10. 移动端测试
// playwright.config.ts - 添加移动端配置
projects: [
// ... 桌面浏览器配置
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] }
}
]11. 自定义测试函数
// tests/e2e/utils.ts
import { Page } from '@playwright/test'
export async function login(page: Page, username: string, password: string) {
await page.goto('/login')
await page.getByLabel('Username').fill(username)
await page.getByLabel('Password').fill(password)
await page.getByRole('button', { name: 'Login' }).click()
}
export async function logout(page: Page) {
await page.getByRole('button', { name: 'Logout' }).click()
}// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
import { login, logout } from './utils'
test('login and logout flow', async ({ page }) => {
// 登录
await login(page, 'testuser', 'password123')
// 断言登录成功
await expect(page.getByText('Welcome, testuser!')).toBeVisible()
// 登出
await logout(page)
// 断言登出成功
await expect(page.getByText('Login')).toBeVisible()
})最佳实践
1. 测试命名规范
- 测试文件名:使用
.spec.ts后缀 - 测试套件:使用
test.describe组织相关测试 - 测试用例:使用清晰、描述性的名称
- 使用
test而不是it定义测试用例
2. 定位策略
- 优先使用
getByRole定位可访问元素 - 使用
getByText定位文本内容 - 使用
getByLabel定位表单元素 - 使用
getByTestId定位带有data-testid属性的元素
3. 测试隔离原则
- 每个测试应该独立运行,不依赖其他测试的状态
- 使用
test.beforeEach和test.afterEach管理测试状态 - 使用
test.beforeAll和test.afterAll管理测试套件状态 - 避免共享可变状态
4. 异步测试最佳实践
- 利用 Playwright 的自动等待特性
- 避免使用
setTimeout - 使用
await expect()等待元素可见或满足条件 - 使用
page.waitForResponse()等待网络请求完成
5. 网络请求处理
- 使用
page.route()拦截和模拟 API 请求 - 使用
page.waitForRequest()等待特定请求 - 使用
page.waitForResponse()等待特定响应 - 为测试环境配置专门的 API 端点
6. 性能测试
- 使用
page.tracing.start()和page.tracing.stop()记录性能追踪 - 测量页面加载时间
- 监控 API 请求响应时间
- 分析关键渲染路径
7. CI/CD 集成
- 在 CI 环境中使用无头模式运行测试
- 配置测试报告生成
- 设置适当的超时和重试策略
- 集成测试结果到代码审查流程
8. 测试数据管理
- 使用测试数据生成器
- 为不同测试场景准备不同的测试数据
- 清理测试产生的数据
- 考虑使用测试数据库
常见问题和解决方案
1. 测试不稳定问题
问题:测试在本地通过,但在 CI 环境中失败
解决方案:
- 增加测试超时时间
- 确保使用自动等待,避免硬编码等待
- 检查 CI 环境的浏览器版本和配置
- 增加重试次数
2. 元素定位问题
问题:无法定位到元素
解决方案:
- 检查元素的可访问性属性
- 使用更具体的定位器
- 确保元素在 DOM 中存在
- 检查元素是否被其他元素覆盖
3. 网络请求问题
问题:API 请求失败或超时
解决方案:
- 使用
page.route()模拟 API 响应 - 检查网络连接和 API 端点
- 增加请求超时时间
- 检查 CORS 配置
4. 截图对比失败
问题:视觉回归测试失败,截图不匹配
解决方案:
- 更新基准截图
- 调整
maxDiffPixelRatio允许更大的差异 - 确保测试环境一致
- 排除动态内容区域
5. 测试速度问题
问题:测试套件运行缓慢
解决方案:
- 并行运行测试
- 减少单个测试文件的测试数量
- 优化测试数据生成
- 避免在测试中使用真实的数据库或网络请求
进阶学习资源
官方文档:
工具和库:
- @playwright/test - Playwright 测试框架
- playwright-testing-library - 结合 Testing Library 的 Playwright 扩展
- faker.js - 测试数据生成器
学习案例:
最佳实践指南:
实践练习
练习 1:基础测试套件
- 初始化一个 Vue 3 + Playwright 项目
- 配置 Playwright 和测试环境
- 编写基础的页面导航测试
- 测试表单提交功能
练习 2:高级交互测试
- 实现一个包含文件上传的组件
- 编写文件上传测试
- 测试拖拽功能
- 测试键盘快捷键
练习 3:网络拦截和模拟
- 创建一个依赖外部 API 的组件
- 使用
page.route()模拟不同的 API 响应 - 测试组件的各种状态(加载、成功、错误)
- 测试 API 错误处理
练习 4:视觉回归测试
- 实现一个复杂的 UI 组件
- 编写视觉回归测试
- 故意修改组件样式,观察测试失败
- 更新基准截图,验证测试通过
练习 5:移动端和多浏览器测试
- 配置移动端测试
- 在不同浏览器中运行测试
- 测试响应式设计
- 分析测试结果和性能
总结
Playwright 是一款强大的 E2E 测试框架,与 Vue 3 应用完美兼容。通过掌握 Playwright 的高级特性和最佳实践,你可以构建全面、可靠的 E2E 测试套件,确保你的 Vue 3 应用在各种环境和浏览器中正常工作。从基础配置到高级功能,从页面交互到网络拦截,Playwright 提供了完整的 E2E 测试解决方案。
下一集我们将学习 Vue 3 与 Cypress 高级 E2E 测试,敬请期待!