bulletproof-react测试策略:端到端测试与单元测试完整方案

bulletproof-react测试策略:端到端测试与单元测试完整方案

【免费下载链接】bulletproof-react 一个简单、可扩展且功能强大的架构,用于构建生产就绪的 React 应用程序。 【免费下载链接】bulletproof-react 项目地址: https://gitcode.com/GitHub_Trending/bu/bulletproof-react

🎯 为什么需要完整的测试策略?

在现代React应用开发中,测试不再是可选项而是必需品。一个健壮的测试策略能够:

  • 提升代码质量:及早发现和修复bug
  • 增强开发信心:重构时确保功能正常
  • 提高交付速度:自动化测试减少手动回归
  • 改善协作效率:测试用例作为活文档

📊 测试金字塔:构建分层测试体系

mermaid

测试类型对比表

测试类型覆盖范围执行速度维护成本置信度
单元测试单个组件/函数⚡️ 极快中等
集成测试组件间交互🏃 快速中等
端到端测试完整用户流程🐢 慢极高

🧪 单元测试:基础构建块

核心工具栈

// package.json 测试依赖
{
  "devDependencies": {
    "@testing-library/react": "^15.0.5",
    "@testing-library/user-event": "^14.5.2",
    "@testing-library/jest-dom": "^6.4.2",
    "vitest": "^2.1.4",
    "jsdom": "^24.0.0"
  }
}

组件测试最佳实践

// 确认对话框组件测试示例
import { Button } from '@/components/ui/button';
import { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';
import { ConfirmationDialog } from '../confirmation-dialog';

test('should handle confirmation flow', async () => {
  const titleText = 'Are you sure?';
  const bodyText = 'Are you sure you want to delete this item?';
  const confirmationButtonText = 'Confirm';
  const openButtonText = 'Open';

  await rtlRender(
    <ConfirmationDialog
      icon="danger"
      title={titleText}
      body={bodyText}
      confirmButton={<Button>{confirmationButtonText}</Button>}
      triggerButton={<Button>{openButtonText}</Button>}
    />,
  );

  // 初始状态验证
  expect(screen.queryByText(titleText)).not.toBeInTheDocument();

  // 用户交互模拟
  await userEvent.click(screen.getByRole('button', { name: openButtonText }));

  // 对话框显示验证
  expect(await screen.findByText(titleText)).toBeInTheDocument();
  expect(screen.getByText(bodyText)).toBeInTheDocument();

  // 取消操作验证
  await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));

  await waitFor(() =>
    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),
  );
  expect(screen.queryByText(bodyText)).not.toBeInTheDocument();
});

测试工具函数封装

// test-utils.tsx 核心工具函数
export const waitForLoadingToFinish = () =>
  waitForElementToBeRemoved(
    () => [
      ...screen.queryAllByTestId(/loading/i),
      ...screen.queryAllByText(/loading/i),
    ],
    { timeout: 4000 },
  );

export const renderApp = async (
  ui: any,
  { user, url = '/', path = '/', ...renderOptions }: Record<string, any> = {},
) => {
  const initializedUser = await initializeUser(user);
  
  const router = createMemoryRouter(
    [{ path: path, element: ui }],
    { initialEntries: url ? ['/', url] : ['/'], initialIndex: url ? 1 : 0 }
  );

  const returnValue = {
    ...rtlRender(ui, {
      wrapper: () => (
        <AppProvider>
          <RouterProvider router={router} />
        </AppProvider>
      ),
      ...renderOptions,
    }),
    user: initializedUser,
  };

  await waitForLoadingToFinish();
  return returnValue;
};

🌐 端到端测试:用户旅程验证

Playwright配置优化

// playwright.config.ts
export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    trace: 'on-first-retry',
  },

  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      testMatch: /.*\.spec\.ts/,
      use: { ...devices['Desktop Chrome'], storageState: 'e2e/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],

  webServer: {
    command: `yarn dev --port ${PORT}`,
    timeout: 10 * 1000,
    port: PORT,
    reuseExistingServer: !process.env.CI,
  },
});

完整的用户流程测试

// smoke.spec.ts - 完整的应用冒烟测试
import { test, expect } from '@playwright/test';
import { createDiscussion, createComment } from '../../src/testing/data-generators';

test('smoke', async ({ page }) => {
  const discussion = createDiscussion();
  const comment = createComment();

  // 导航到首页
  await page.goto('/');
  await page.getByRole('button', { name: 'Get started' }).click();
  await page.waitForURL('/app');

  // 创建讨论
  await page.getByRole('link', { name: 'Discussions' }).click();
  await page.waitForURL('/app/discussions');
  await page.getByRole('button', { name: 'Create Discussion' }).click();
  
  await page.getByLabel('Title').click();
  await page.getByLabel('Title').fill(discussion.title);
  await page.getByLabel('Body').click();
  await page.getByLabel('Body').fill(discussion.body);
  await page.getByRole('button', { name: 'Submit' }).click();

  // 验证讨论创建成功
  await expect(page.getByRole('heading', { name: discussion.title })).toBeVisible();
  await expect(page.getByText(discussion.body)).toBeVisible();

  // 更新讨论
  await page.getByRole('button', { name: 'Update Discussion' }).click();
  await page.getByLabel('Title').fill(`${discussion.title} - updated`);
  await page.getByLabel('Body').fill(`${discussion.body} - updated`);
  await page.getByRole('button', { name: 'Submit' }).click();

  // 创建和删除评论
  await page.getByRole('button', { name: 'Create Comment' }).click();
  await page.getByLabel('Body').fill(comment.body);
  await page.getByRole('button', { name: 'Submit' }).click();
  await expect(page.getByText(comment.body)).toBeVisible();

  // 删除评论
  await page.getByRole('button', { name: 'Delete Comment' }).click();
  await page.getByRole('button', { name: 'Delete Comment' }).click();
  await expect(page.getByText(comment.body)).toBeHidden();

  // 删除讨论
  await page.getByRole('link', { name: 'Discussions' }).click();
  await page.getByRole('button', { name: 'Delete Discussion' }).click();
  await page.getByRole('button', { name: 'Delete Discussion' }).click();
  await expect(page.getByRole('heading', { name: 'No Entries Found' })).toBeVisible();
});

🔧 Mock服务器:测试环境隔离

MSW配置和数据生成

// 数据生成器 - 创建测试数据
export const createUser = (userProperties?: any) => ({
  id: faker.string.uuid(),
  email: faker.internet.email(),
  firstName: faker.person.firstName(),
  lastName: faker.person.lastName(),
  teamId: faker.helpers.arrayElement(['1', '2', '3']),
  teamName: faker.helpers.arrayElement(['Design', 'Development', 'Marketing']),
  role: faker.helpers.arrayElement(['USER', 'ADMIN']),
  bio: faker.person.bio(),
  createdAt: faker.date.past(),
  ...userProperties,
});

// API处理器 - 模拟后端响应
export const handlers = [
  rest.post(`${config.apiUrl}/auth/login`, async (req, res, ctx) => {
    try {
      const { email, password } = await req.json();
      const user = db.user.findFirst({ where: { email: { equals: email } } });
      
      if (!user || user.password !== hash(password)) {
        return res(ctx.status(401), ctx.json({ message: 'Invalid credentials' }));
      }
      
      const jwt = faker.string.uuid();
      return res(ctx.json({ jwt, user: omit(user, ['password']) }));
    } catch (error) {
      return res(ctx.status(500), ctx.json({ message: 'Server Error' }));
    }
  }),
];

🚀 CI/CD集成策略

测试脚本配置

{
  "scripts": {
    "test": "vitest",
    "test:unit": "vitest --config vitest.config.unit.ts",
    "test:integration": "vitest --config vitest.config.integration.ts",
    "test:e2e": "pm2 start \"yarn run-mock-server\" --name server && playwright test",
    "test:ci": "yarn test:unit && yarn test:integration && yarn test:e2e",
    "test:watch": "vitest --watch",
    "test:coverage": "vitest --coverage"
  }
}

分层测试执行流程

mermaid

📈 测试覆盖率与质量指标

覆盖率目标设定

测试类型覆盖率目标关键指标
单元测试80%+组件逻辑覆盖率
集成测试70%+用户交互覆盖率
端到端测试50%+核心流程覆盖率

质量门禁配置

# 在CI pipeline中设置质量门禁
npm test -- --coverage --coverageThreshold='{
  "global": {
    "branches": 80,
    "functions": 80,
    "lines": 80,
    "statements": 80
  }
}'

🎯 最佳实践总结

1. 测试优先级策略

  • 优先编写集成测试:覆盖用户真实交互场景
  • 关键业务单元测试:核心逻辑必须单元测试
  • 端到端测试精选:只测试最重要的用户旅程

2. 测试数据管理

  • 使用工厂函数创建测试数据
  • 保持测试数据的一致性
  • 避免硬编码的测试数据

3. 测试维护策略

  • 定期重构测试代码
  • 删除过时和重复的测试
  • 保持测试代码的可读性

4. 性能优化

  • 并行执行测试用例
  • 使用测试双精度(Test Doubles)
  • 避免不必要的渲染和网络请求

通过这套完整的测试策略,bulletproof-react项目能够确保代码质量,提高开发效率,并为持续交付提供坚实保障。记住:好的测试不是负担,而是开发效率的倍增器。

【免费下载链接】bulletproof-react 一个简单、可扩展且功能强大的架构,用于构建生产就绪的 React 应用程序。 【免费下载链接】bulletproof-react 项目地址: https://gitcode.com/GitHub_Trending/bu/bulletproof-react

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值