38dc055a27
- 添加菜单数据修复设计文档 - 添加用户管理和角色管理测试修复设计文档 - 添加本地开发测试设计文档 - 添加相关实现计划
8.1 KiB
8.1 KiB
E2E 测试最佳实践
概述
本文档记录了 NovaVis 睿视项目的 E2E 测试最佳实践。
核心原则
1. 测试应该验证真实用户行为
问题:测试只验证元素存在,不验证实际功能
解决方案:
// ❌ 错误:只验证元素可见
const button = page.locator('button')
await expect(button).toBeVisible()
// ✅ 正确:验证实际功能
const button = page.locator('button:has-text("进入")').first()
await button.scrollIntoViewIfNeeded()
await button.click({ force: true })
// 验证页面跳转
await page.waitForURL('**/overview')
const title = await page.locator('h1').textContent()
expect(title).toContain('概览')
2. 使用硬验证而非软验证
问题:测试使用软验证,即使页面没有内容也能通过
解决方案:
// ❌ 错误:软验证
const dataStats = page.locator('[data-testid="data-stats"]')
if (await dataStats.isVisible()) {
const statsText = await dataStats.textContent()
expect(statsText).toBeTruthy()
}
// ✅ 正确:硬验证
const dataStats = page.locator('[data-testid="data-stats"]')
await dataStats.waitFor({ state: 'visible', timeout: 5000 })
const statsText = await dataStats.textContent()
expect(statsText).toBeTruthy()
expect(statsText!.length).toBeGreaterThan(10)
3. 验证数据流而非仅 UI
问题:测试只验证 UI,不验证数据是否正确加载
解决方案:
// ❌ 错误:只验证 UI
const table = page.locator('.ant-table')
await expect(table).toBeVisible()
// ✅ 正确:验证数据流
const table = page.locator('.ant-table')
await expect(table).toBeVisible()
// 验证表格有数据
const rows = await table.locator('.ant-table-row').count()
expect(rows).toBeGreaterThan(0)
// 验证数据内容
const firstRowText = await table.locator('.ant-table-row').first().textContent()
expect(firstRowText).toBeTruthy()
expect(firstRowText!.length).toBeGreaterThan(5)
测试结构
1. 使用 test.step 组织测试步骤
test('应该能够导入数据', async ({ page }) => {
await test.step('1. 导航到数据导入页面', async () => {
await page.goto('/data-import')
await page.waitForLoadState('networkidle')
})
await test.step('2. 选择文件', async () => {
const fileInput = page.locator('input[type="file"]')
await fileInput.setInputFiles('test-data/sample.xlsx')
})
await test.step('3. 验证导入成功', async () => {
const successMessage = page.locator('.ant-message-success')
await expect(successMessage).toBeVisible()
})
})
2. 使用 Page Object Model
// pages/DataImportPage.ts
export class DataImportPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('/data-import')
await this.page.waitForLoadState('networkidle')
}
async selectFile(filePath: string) {
const fileInput = this.page.locator('input[type="file"]')
await fileInput.setInputFiles(filePath)
}
async verifyImportSuccess() {
const successMessage = this.page.locator('.ant-message-success')
await expect(successMessage).toBeVisible()
}
}
3. 使用 Workflow 封装业务流程
// workflows/DataImportWorkflow.ts
export class DataImportWorkflow {
constructor(private page: Page) {}
async importFile(filePath: string) {
await this.navigateToImport()
await this.selectFile(filePath)
await this.verifyImportSuccess()
}
private async navigateToImport() {
// 导航逻辑
}
private async selectFile(filePath: string) {
// 选择文件逻辑
}
private async verifyImportSuccess() {
// 验证逻辑
}
}
选择器策略
1. 优先级
-
文本选择器 - 最稳定
page.locator('button:has-text("提交")') -
角色选择器 - 语义化
page.getByRole('button', { name: '提交' }) -
标签选择器 - 简单
page.locator('h1') -
data-testid - 最后选择
page.locator('[data-testid="submit-button"]')
2. 避免使用的选择器
❌ CSS 类名 - 容易变化
page.locator('.ant-btn-primary')
❌ 复杂的 CSS 选择器 - 脆弱
page.locator('div > div > button.ant-btn.ant-btn-primary')
等待策略
1. 使用自动等待
// ✅ 推荐:Playwright 自动等待
await page.click('button')
// ❌ 不推荐:手动等待
await page.waitForTimeout(1000)
await page.click('button')
2. 明确等待条件
// ✅ 推荐:明确等待条件
await page.waitForSelector('.ant-table-row', { state: 'visible' })
// ❌ 不推荐:模糊等待
await page.waitForTimeout(2000)
3. 等待网络请求
// ✅ 推荐:等待网络请求完成
await page.waitForLoadState('networkidle')
// 或者等待特定请求
await page.waitForResponse(response =>
response.url().includes('/api/data') && response.status() === 200
)
错误处理
1. 使用 try-catch 处理可选操作
try {
const optionalButton = page.locator('button:has-text("可选操作")')
await optionalButton.click({ timeout: 5000 })
} catch (error) {
console.log('可选操作按钮不存在,跳过')
}
2. 使用条件判断
const cancelButton = page.locator('button:has-text("取消")')
if (await cancelButton.isVisible()) {
await cancelButton.click()
}
调试技巧
1. 使用页面快照
const snapshot = await page.locator('body').innerHTML()
console.log('Page snapshot:', snapshot)
2. 使用截图
await page.screenshot({ path: 'debug.png', fullPage: true })
3. 使用 trace
// 在 playwright.config.ts 中启用
use: {
trace: 'on-first-retry',
}
测试数据
1. 使用测试数据工厂
// fixtures/test-data-factory.ts
export class TestDataFactory {
async createExcel(rows: number): Promise<string> {
// 创建测试 Excel 文件
}
async createLargeExcel(rows: number): Promise<string> {
// 创建大型测试 Excel 文件
}
async createCorruptedExcel(): Promise<string> {
// 创建损坏的 Excel 文件
}
}
2. 使用 fixtures
// fixtures/test-fixtures.ts
import { test as base } from '@playwright/test'
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// 登录逻辑
await page.goto('/login')
await page.fill('input[name="username"]', 'testuser')
await page.fill('input[name="password"]', 'password')
await page.click('button[type="submit"]')
await use(page)
},
})
性能优化
1. 并行执行
// playwright.config.ts
export default defineConfig({
workers: 4, // 并行执行 4 个测试
})
2. 重用登录状态
// 使用 storageState 重用登录状态
export default defineConfig({
use: {
storageState: 'auth.json',
},
})
3. 跳过不必要的等待
// ❌ 不推荐:全局等待
await page.waitForTimeout(2000)
// ✅ 推荐:精确等待
await page.waitForSelector('.ant-table-row')
CI/CD 集成
1. 使用 Docker
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx playwright install --with-deps
2. 使用 GitHub Actions
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
总结
遵循这些最佳实践,可以确保 E2E 测试:
- ✅ 验证真实用户行为
- ✅ 使用硬验证
- ✅ 验证数据流
- ✅ 使用稳定的选择器
- ✅ 正确处理等待
- ✅ 良好的错误处理
- ✅ 易于调试
- ✅ 性能优化
- ✅ CI/CD 友好