第 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 jsdomvitest.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 mswmocks/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 |
| Hook | renderHook + act |
| HTTP mock | MSW 拦截 |
| 快照 | 谨慎用 |
| CI | --run 非 watch |
动手练习
- Button 测试:点击触发回调
- Todo 测试:增删改、勾选完成、过滤已完成
- 自定义 Hook 测试:测试
useToggle/useLocalStorage - MSW:mock
/api/users,测试加载/错误/空三种状态
下一章:第 25 章:部署与进阶 →