Skip to content
第 163 / 250 章前端⏱ 10 分钟阅读

第 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 -- --coverage
ts
// 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
Mockvi.fn() / vi.mock()
覆盖率v8

动手练习

  1. 给 Counter 组件写测试
  2. 测试表单提交
  3. 用 mock 测试 fetch
  4. 用 renderHook 测试自定义 hook
  5. 生成覆盖率报告

推荐阅读


下一章:第 164 章:性能优化与最佳实践

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