第 163 章:Vitest 测试
学习目标
- 掌握 Vitest 单元测试
- 学会 React Testing Library 组件测试
- 理解 Mock 与 spy
- 学会覆盖率报告
一、为什么测试
二、安装
bash
pnpm add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom三、配置
3.1 vitest.config.ts
ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './'),
},
},
});3.2 vitest.setup.ts
ts
import '@testing-library/jest-dom/vitest';3.3 tsconfig.json
json
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
}
}四、第一个测试
4.1 被测组件
tsx
// components/Counter.tsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>计数:{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</>
);
}4.2 测试
tsx
// components/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter', () => {
it('初始显示 0', () => {
render(<Counter />);
expect(screen.getByText('计数:0')).toBeInTheDocument();
});
it('点击 +1 后增加', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: '+1' });
await user.click(button);
expect(screen.getByText('计数:1')).toBeInTheDocument();
});
});五、查询元素
tsx
// 按角色(推荐)
screen.getByRole('button', { name: '提交' });
screen.getByRole('textbox', { name: '邮箱' });
// 按文本
screen.getByText('你好');
screen.getByText(/hello/i);
// 按 Label
screen.getByLabelText('用户名');
// 按占位符
screen.getByPlaceholderText('搜索');
// 按 TestId
screen.getByTestId('submit-btn');
// 多个
screen.getAllByRole('listitem');六、用户交互
tsx
import userEvent from '@testing-library/user-event';
it('输入框输入文字', async () => {
const user = userEvent.setup();
render(<Input />);
const input = screen.getByRole('textbox');
await user.type(input, 'Hello');
expect(input).toHaveValue('Hello');
});
it('提交表单', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<Form onSubmit={handleSubmit} />);
await user.click(screen.getByRole('button', { name: '提交' }));
expect(handleSubmit).toHaveBeenCalledOnce();
});七、Mock
7.1 Mock 函数
tsx
import { vi } from 'vitest';
it('点击时调用回调', async () => {
const onClick = vi.fn();
const user = userEvent.setup();
render(<Button onClick={onClick} />);
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});7.2 Mock 模块
tsx
// Mock 整个模块
vi.mock('@/lib/api', () => ({
fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: 'Tom' })),
}));
import { fetchUser } from '@/lib/api';
it('展示用户', async () => {
render(<UserProfile userId={1} />);
expect(await screen.findByText('Tom')).toBeInTheDocument();
});7.3 Mock fetch
tsx
beforeEach(() => {
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'x' }),
} as Response)
);
});八、异步测试
tsx
it('加载完成后显示数据', async () => {
render(<UserList />);
// 等待元素出现
const item = await screen.findByText('Tom');
expect(item).toBeInTheDocument();
});九、Hook 测试
tsx
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
it('useCounter 初始为 0', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('调用 inc 增加', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.inc();
});
expect(result.current.count).toBe(1);
});十、覆盖率
bash
pnpm test -- --coveragets
// vitest.config.ts
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
include: ['components/**', 'app/**', 'lib/**'],
},
}十一、运行测试
bash
# package.json scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}十二、Next.js 注意事项
12.1 Server Components
Server Components 直接测试较难,通常测 Client Components 或用 Playwright E2E。
12.2 Client Component 测试
tsx
'use client';
import { useState } from 'react';
export function Toggle() {
const [on, setOn] = useState(false);
return <button onClick={() => setOn(!on)}>{on ? 'ON' : 'OFF'}</button>;
}tsx
import { Toggle } from './Toggle';
it('切换', async () => {
const user = userEvent.setup();
render(<Toggle />);
await user.click(screen.getByRole('button'));
expect(screen.getByText('ON')).toBeInTheDocument();
});十三、本章小结
| 概念 | 工具 |
|---|---|
| 单元测试 | Vitest |
| 组件测试 | React Testing Library |
| 交互 | @testing-library/user-event |
| Mock | vi.fn() / vi.mock() |
| 覆盖率 | v8 |
动手练习
- 给 Counter 组件写测试
- 测试表单提交
- 用 mock 测试 fetch
- 用 renderHook 测试自定义 hook
- 生成覆盖率报告
推荐阅读
下一章:第 164 章:性能优化与最佳实践 →