web-ui-testing-standards.md 11 KB

Web UI 包测试规范

版本信息

版本 日期 描述 作者
1.0 2025-12-26 从测试策略文档拆分,专注Web UI包测试 James (Claude Code)

概述

本文档定义了Web UI包的测试标准和最佳实践,包括:

  • packages/*-ui: Web UI组件包(用户管理UI、文件管理UI等)
  • web/tests/: Web应用测试(组件测试、集成测试、E2E测试)

测试框架栈

单元测试和集成测试

  • Vitest: 测试运行器
  • Testing Library: React组件测试(@testing-library/react@testing-library/user-event
  • Happy DOM: 轻量级DOM环境(@happy-dom/global-registrator

E2E测试

  • Playwright: 端到端测试框架

测试分层策略

1. 组件测试(Component Tests)

  • 范围: 单个UI组件
  • 目标: 验证组件渲染、交互和状态
  • 位置: packages/*-ui/tests/**/*.test.tsxweb/tests/unit/client/**/*.test.tsx
  • 框架: Vitest + Testing Library
  • 覆盖率目标: ≥ 80%

2. 集成测试(Integration Tests)

  • 范围: 多个组件协作、与API集成
  • 目标: 验证组件间交互和数据流
  • 位置: web/tests/integration/**/*.test.tsx
  • 框架: Vitest + Testing Library + MSW(API模拟)
  • 覆盖率目标: ≥ 60%

3. E2E测试(End-to-End Tests)

  • 范围: 完整用户流程
  • 目标: 验证端到端业务流程
  • 位置: web/tests/e2e/**/*.test.ts
  • 框架: Playwright
  • 覆盖率目标: 关键用户流程100%

测试文件结构

UI包结构

packages/user-management-ui/
├── src/
│   ├── components/
│   │   ├── UserTable.tsx
│   │   └── UserForm.tsx
│   └── index.ts
└── tests/
    ├── unit/
    │   ├── UserTable.test.tsx
    │   └── UserForm.test.tsx
    └── integration/
        └── UserManagementFlow.test.tsx

Web应用结构

web/tests/
├── unit/
│   └── client/
│       ├── pages/
│       │   └── Users.test.tsx
│       └── components/
│           └── DataTablePagination.test.tsx
├── integration/
│   └── client/
│       └── user-management.test.tsx
└── e2e/
    ├── login.spec.ts
    └── user-management.spec.ts

组件测试最佳实践

1. 使用Testing Library原则

// ✅ 推荐:从用户角度测试
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('应该允许用户创建新用户', async () => {
  const user = userEvent.setup();
  render(<UserForm />);

  // 填写表单
  await user.type(screen.getByLabelText(/用户名/), 'testuser');
  await user.type(screen.getByLabelText(/邮箱/), 'test@example.com');

  // 提交表单
  await user.click(screen.getByRole('button', { name: /创建/ }));

  // 验证结果
  expect(screen.getByText(/创建成功/)).toBeInTheDocument();
});

// ❌ 避免:测试实现细节
test('submitButton onClick 应该被调用', () => {
  const mockOnClick = vi.fn();
  render(<Button onClick={mockOnClick}>提交</Button>);

  fireEvent.click(screen.getByText('提交'));
  expect(mockOnClick).toHaveBeenCalled();
});

2. 使用数据测试ID

// 组件代码
<Button data-testid="submit-button" type="submit">提交</Button>

// 测试代码
const submitButton = screen.getByTestId('submit-button');
await user.click(submitButton);

3. 模拟API调用

import { vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { UserForm } from './UserForm';

// 模拟RPC客户端
vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
  rpcClient: vi.fn(() => ({
    users: {
      create: {
        $post: vi.fn(() => Promise.resolve({
          status: 201,
          json: async () => ({ id: 1, username: 'testuser' })
        }))
      }
    }
  }))
}));

4. 测试异步状态

test('应该显示加载状态', async () => {
  render(<UserList />);

  // 初始加载状态
  expect(screen.getByText(/加载中/)).toBeInTheDocument();

  // 等待数据加载完成
  await waitFor(() => {
    expect(screen.getByText(/用户列表/)).toBeInTheDocument();
  });
});

集成测试最佳实践

1. 使用真实组件而非模拟

// ✅ 推荐:使用真实组件,模拟API
vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
  rpcClient: vi.fn(() => ({
    users: {
      $get: vi.fn(() => Promise.resolve({
        status: 200,
        json: async () => ({ data: mockUsers })
      }))
    }
  }))
}));

// ❌ 避免:模拟整个组件
vi.mock('@d8d/user-management-ui', () => ({
  UserTable: () => <div data-testid="mock-user-table" />
}));

2. 提供完整的模拟数据

const mockUsers = [
  {
    id: 1,
    username: 'testuser',
    email: 'test@example.com',
    role: 'user',
    createdAt: '2025-01-01T00:00:00.000Z',
    // 包含真实组件需要的所有字段
  }
];

3. 验证完整的用户流程

test('应该成功创建用户并刷新列表', async () => {
  const user = userEvent.setup();
  render(<UserManagementPage />);

  // 打开创建表单
  await user.click(screen.getByTestId('create-user-button'));

  // 填写表单
  await user.type(screen.getByLabelText(/用户名/), 'newuser');
  await user.type(screen.getByLabelText(/邮箱/), 'new@example.com'));

  // 提交表单
  await user.click(screen.getByRole('button', { name: /创建/ }));

  // 验证成功消息
  await waitFor(() => {
    expect(screen.getByText(/创建成功/)).toBeInTheDocument();
  });

  // 验证列表刷新
  await waitFor(() => {
    expect(screen.getByText('newuser')).toBeInTheDocument();
  });
});

4. 使用共享测试工具处理复杂组件

import { completeRadixSelectFlow } from '@d8d/shared-ui-components/tests/utils';

// 处理Radix UI Select组件的完整选择流程
await completeRadixSelectFlow('role-selector', 'admin', { useFireEvent: true });

E2E测试最佳实践

1. 使用Page Object模式

// tests/e2e/pages/login.page.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/login');
  }

  async login(username: string, password: string) {
    await this.page.fill('input[name="username"]', username);
    await this.page.fill('input[name="password"]', password);
    await this.page.click('button[type="submit"]');
  }

  async expectWelcomeMessage() {
    await expect(this.page.getByText(/欢迎/)).toBeVisible();
  }
}

// 测试文件
test('用户登录流程', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('testuser', 'password');
  await loginPage.expectWelcomeMessage();
});

2. 使用稳定的定位器

// ✅ 推荐:使用语义化定位器
await page.click('button:has-text("提交")');
await page.fill('input[placeholder="请输入用户名"]', 'testuser');
await page.click('[data-testid="submit-button"]');

// ❌ 避免:使用不稳定的CSS选择器
await page.click('.btn-primary');

3. 等待策略

// 等待元素可见
await page.waitForSelector('[data-testid="user-list"]');

// 等待网络请求完成
await page.waitForLoadState('networkidle');

// 等待特定条件
await page.waitForURL('/dashboard');

测试命名约定

文件命名

  • 组件测试:[ComponentName].test.tsx
  • 集成测试:[feature].integration.test.tsx
  • E2E测试:[feature].spec.ts

测试描述

describe('UserForm', () => {
  describe('表单验证', () => {
    it('应该验证必填字段', async () => { });
    it('应该验证邮箱格式', async () => { });
  });

  describe('表单提交', () => {
    it('应该成功创建用户', async () => { });
    it('应该处理网络错误', async () => { });
  });
});

常见错误避免

❌ 不要测试实现细节

// 错误:测试useState调用
expect(useState).toHaveBeenCalledWith([]);

// 正确:测试渲染结果
expect(screen.getByText('用户列表')).toBeInTheDocument();

❌ 不要过度模拟

// 错误:模拟整个组件库
vi.mock('@d8d/shared-ui-components', () => ({
  Button: () => <button data-testid="mock-button" />
}));

// 正确:使用真实组件,模拟其依赖

❌ 不要忽略异步操作

// 错误:不等待异步操作
fireEvent.click(submitButton);
expect(successMessage).toBeInTheDocument();

// 正确:等待异步操作完成
await user.click(submitButton);
await waitFor(() => {
  expect(screen.getByText(/成功/)).toBeInTheDocument();
});

覆盖率标准

测试类型 最低要求 目标要求
组件测试 70% 80%
集成测试 50% 60%
E2E测试 关键流程100% 主要流程80%

运行测试

本地开发

# 运行所有测试
pnpm test

# 运行组件测试
pnpm test:components

# 运行集成测试
pnpm test:integration

# 运行E2E测试
pnpm test:e2e:chromium

# 生成覆盖率报告
pnpm test:coverage

# 运行特定测试
pnpm test --testNamePattern="UserForm"

CI/CD

web-component-tests:
  runs-on: ubuntu-latest
  steps:
    - run: cd web && pnpm test:components

web-integration-tests:
  runs-on: ubuntu-latest
  steps:
    - run: cd web && pnpm test:integration

web-e2e-tests:
  runs-on: ubuntu-latest
  steps:
    - run: cd web && pnpm test:e2e:chromium

调试技巧

1. 使用调试模式

# 运行特定测试并显示详细信息
pnpm test --testNamePattern="UserForm" --reporter=verbose

# 在浏览器中打开调试器
pnpm test:components --debug

2. 查看DOM结构

// 在测试中打印DOM结构
screen.debug();

// 打印特定元素
screen.debug(screen.getByTestId('user-table'));

3. E2E测试调试

// 使用调试模式
test('调试模式', async ({ page }) => {
  await page.goto('/users');
  await page.pause(); // 暂停执行,打开Playwright Inspector
});

参考实现

  • 用户管理UI包:packages/user-management-ui
  • 文件管理UI包:packages/file-management-ui
  • Web应用测试:web/tests/

相关文档


文档状态: 正式版 适用范围: Web UI包和Web应用