docs: 添加设计文档和实现计划
- 添加菜单数据修复设计文档 - 添加用户管理和角色管理测试修复设计文档 - 添加本地开发测试设计文档 - 添加相关实现计划
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
# 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 友好
|
||||
Reference in New Issue
Block a user