Files
张翔 38dc055a27 docs: 添加设计文档和实现计划
- 添加菜单数据修复设计文档
- 添加用户管理和角色管理测试修复设计文档
- 添加本地开发测试设计文档
- 添加相关实现计划
2026-04-15 23:36:27 +08:00

8.1 KiB
Raw Permalink Blame History

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. 优先级

  1. 文本选择器 - 最稳定

    page.locator('button:has-text("提交")')
    
  2. 角色选择器 - 语义化

    page.getByRole('button', { name: '提交' })
    
  3. 标签选择器 - 简单

    page.locator('h1')
    
  4. 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 测试:

  1. 验证真实用户行为
  2. 使用硬验证
  3. 验证数据流
  4. 使用稳定的选择器
  5. 正确处理等待
  6. 良好的错误处理
  7. 易于调试
  8. 性能优化
  9. CI/CD 友好