Skip to content
第 24 章 前端 ⏱ 14 分钟阅读

第 24 章:测试 ​

学习目标 ​

  • 掌握 Vitest + React Testing Library
  • 写组件测试、Hook 测试、异步测试
  • 用 MSW mock HTTP
  • 避开测试的 5 个常见坑

一、为什么测 React? ​

  • 组件改 UI 时不会被破坏
  • Hooks 逻辑稳定
  • 重构有信心
  • 文档作用(测试即用例)

二、Vitest + RTL 安装 ​

bash
npm install -D vitest @testing-library/react @testing-library/dom @testing-library/jest-dom jsdom

vitest.config.ts:

ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./vitest.setup.ts'],
    globals: true,
  },
});

vitest.setup.ts:

ts
import '@testing-library/jest-dom';

三、第一个组件测试 ​

tsx
// Button.tsx
export function Button({ label, onClick }: { label: string; onClick: () => void }) {
  return <button onClick={onClick}>{label}</button>;
}

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

test('点击触发回调', () => {
  const onClick = vi.fn();
  render(<Button label="点我" onClick={onClick} />);

  const btn = screen.getByText('点我');
  fireEvent.click(btn);

  expect(onClick).toHaveBeenCalledTimes(1);
});

四、查询元素 ​

tsx
// 按文本
screen.getByText('登录');                  // 精确
screen.getByText(/登/i);                   // 正则
screen.queryByText('...');                  // 不存在返回 null

// 按 role(推荐,可访问性)
screen.getByRole('button', { name: '登录' });
screen.getByRole('textbox');

// 按 label
screen.getByLabelText('用户名');

// 按 placeholder
screen.getByPlaceholderText('搜索');

⚠️ 坑 1:优先 getByRole(屏幕阅读器友好),次之 getByLabelText,避免 getByTestId(最后手段)。

五、测试用户交互 ​

tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('输入框', async () => {
  const user = userEvent.setup();
  render(<input aria-label="username" />);

  const input = screen.getByRole('textbox', { name: 'username' });
  await user.type(input, '张三');

  expect(input).toHaveValue('张三');
});

⚠️ 坑 2:用 @testing-library/user-event 模拟真实用户行为(逐字符输入),不要用 fireEvent(太粗糙)。

六、测试异步组件 ​

tsx
import { render, screen, waitFor } from '@testing-library/react';

test('加载后显示数据', async () => {
  render(<UserList />);

  // 等待元素出现
  const user = await screen.findByText('张三');
  expect(user).toBeInTheDocument();
});

// 或 waitFor
await waitFor(() => {
  expect(screen.getByText('张三')).toBeInTheDocument();
});

七、Mock 函数 ​

tsx
test('回调被调用', () => {
  const fn = vi.fn();
  fn('hello');
  expect(fn).toHaveBeenCalledWith('hello');
  expect(fn).toHaveBeenCalledTimes(1);
});

test('返回值', () => {
  const fn = vi.fn().mockReturnValue(42);
  expect(fn()).toBe(42);
});

八、测试自定义 Hook ​

tsx
import { renderHook, act } from '@testing-library/react';

test('useToggle', () => {
  const { result } = renderHook(() => useToggle(false));

  expect(result.current[0]).toBe(false);

  act(() => result.current[1]());
  expect(result.current[0]).toBe(true);
});

九、Mock HTTP(MSW) ​

bash
npm install -D msw

mocks/handlers.ts:

ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users', () => HttpResponse.json([{ id: 1, name: '张三' }])),
];

vitest.setup.ts:

ts
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';

export const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

import '@testing-library/jest-dom';

测试中:

tsx
test('加载用户列表', async () => {
  render(<UserList />);
  expect(await screen.findByText('张三')).toBeInTheDocument();
});

十、测试 Context Provider ​

tsx
function renderWithProviders(ui: React.ReactElement) {
  return render(<ThemeProvider><UserProvider>{ui}</UserProvider></ThemeProvider>);
}

test('...', () => {
  renderWithProviders(<MyComp />);
});

十一、快照测试(谨慎用) ​

tsx
test('快照', () => {
  const { container } = render(<Card title="hi" />);
  expect(container).toMatchSnapshot();
});

⚠️ 坑 3:快照很容易因为无关修改挂掉,慎用,只在稳定组件上用。

十二、覆盖率 ​

bash
npx vitest run --coverage

目标覆盖率:

  • 工具函数、reducer:90%+
  • 组件:70%+
  • 业务逻辑:关键路径全覆盖

⚠️ 坑 4:覆盖率不等于测试质量,100% 覆盖也可能漏 bug。

十三、CI 集成 ​

yaml
# .github/workflows/test.yml
- run: npm test -- --run     # ⚠️ CI 用 --run,非 watch

⚠️ 坑 5:vitest 默认 watch,CI 里必须 --run。

十四、本章小结 ​

要点关键
工具Vitest + React Testing Library
查询优先 getByRole,避免 testid
交互userEvent,不要 fireEvent
异步findBy / waitFor
HookrenderHook + act
HTTP mockMSW 拦截
快照谨慎用
CI--run 非 watch

动手练习 ​

  1. Button 测试:点击触发回调
  2. Todo 测试:增删改、勾选完成、过滤已完成
  3. 自定义 Hook 测试:测试 useToggle / useLocalStorage
  4. MSW:mock /api/users,测试加载/错误/空三种状态

下一章:第 25 章:部署与进阶 →

本站基于 VitePress 构建 · 由 StackHub 团队维护