Files
novalon-manage-system/docs/superpowers/guides/e2e-testing-best-practices.md
T
张翔 38dc055a27 docs: 添加设计文档和实现计划
- 添加菜单数据修复设计文档
- 添加用户管理和角色管理测试修复设计文档
- 添加本地开发测试设计文档
- 添加相关实现计划
2026-04-15 23:36:27 +08:00

386 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# E2E 测试最佳实践
## 概述
本文档记录了 NovaVis 睿视项目的 E2E 测试最佳实践。
## 核心原则
### 1. 测试应该验证真实用户行为
**问题**:测试只验证元素存在,不验证实际功能
**解决方案**
```typescript
// ❌ 错误:只验证元素可见
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. 使用硬验证而非软验证
**问题**:测试使用软验证,即使页面没有内容也能通过
**解决方案**
```typescript
// ❌ 错误:软验证
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,不验证数据是否正确加载
**解决方案**
```typescript
// ❌ 错误:只验证 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 组织测试步骤
```typescript
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
```typescript
// 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 封装业务流程
```typescript
// 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. 优先级
1. **文本选择器** - 最稳定
```typescript
page.locator('button:has-text("提交")')
```
2. **角色选择器** - 语义化
```typescript
page.getByRole('button', { name: '提交' })
```
3. **标签选择器** - 简单
```typescript
page.locator('h1')
```
4. **data-testid** - 最后选择
```typescript
page.locator('[data-testid="submit-button"]')
```
### 2. 避免使用的选择器
❌ **CSS 类名** - 容易变化
```typescript
page.locator('.ant-btn-primary')
```
❌ **复杂的 CSS 选择器** - 脆弱
```typescript
page.locator('div > div > button.ant-btn.ant-btn-primary')
```
## 等待策略
### 1. 使用自动等待
```typescript
// ✅ 推荐:Playwright 自动等待
await page.click('button')
// ❌ 不推荐:手动等待
await page.waitForTimeout(1000)
await page.click('button')
```
### 2. 明确等待条件
```typescript
// ✅ 推荐:明确等待条件
await page.waitForSelector('.ant-table-row', { state: 'visible' })
// ❌ 不推荐:模糊等待
await page.waitForTimeout(2000)
```
### 3. 等待网络请求
```typescript
// ✅ 推荐:等待网络请求完成
await page.waitForLoadState('networkidle')
// 或者等待特定请求
await page.waitForResponse(response =>
response.url().includes('/api/data') && response.status() === 200
)
```
## 错误处理
### 1. 使用 try-catch 处理可选操作
```typescript
try {
const optionalButton = page.locator('button:has-text("可选操作")')
await optionalButton.click({ timeout: 5000 })
} catch (error) {
console.log('可选操作按钮不存在,跳过')
}
```
### 2. 使用条件判断
```typescript
const cancelButton = page.locator('button:has-text("取消")')
if (await cancelButton.isVisible()) {
await cancelButton.click()
}
```
## 调试技巧
### 1. 使用页面快照
```typescript
const snapshot = await page.locator('body').innerHTML()
console.log('Page snapshot:', snapshot)
```
### 2. 使用截图
```typescript
await page.screenshot({ path: 'debug.png', fullPage: true })
```
### 3. 使用 trace
```typescript
// 在 playwright.config.ts 中启用
use: {
trace: 'on-first-retry',
}
```
## 测试数据
### 1. 使用测试数据工厂
```typescript
// 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
```typescript
// 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. 并行执行
```typescript
// playwright.config.ts
export default defineConfig({
workers: 4, // 并行执行 4 个测试
})
```
### 2. 重用登录状态
```typescript
// 使用 storageState 重用登录状态
export default defineConfig({
use: {
storageState: 'auth.json',
},
})
```
### 3. 跳过不必要的等待
```typescript
// ❌ 不推荐:全局等待
await page.waitForTimeout(2000)
// ✅ 推荐:精确等待
await page.waitForSelector('.ant-table-row')
```
## CI/CD 集成
### 1. 使用 Docker
```dockerfile
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
```yaml
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 测试:
1. ✅ 验证真实用户行为
2. ✅ 使用硬验证
3. ✅ 验证数据流
4. ✅ 使用稳定的选择器
5. ✅ 正确处理等待
6. ✅ 良好的错误处理
7. ✅ 易于调试
8. ✅ 性能优化
9. ✅ CI/CD 友好