# 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 { // 创建测试 Excel 文件 } async createLargeExcel(rows: number): Promise { // 创建大型测试 Excel 文件 } async createCorruptedExcel(): Promise { // 创建损坏的 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 友好