测什么

类型
纯函数 / 工具 Vitest 单测
Hooks Vitest + @testing-library/reactrenderHook
组件 Vitest + React Testing Library
端到端 Playwright(覆盖关键路径,3-10 条就够)
视觉回归 Chromatic / Percy(可选)

安装

npm i -D vitest @vitest/ui jsdom \
        @testing-library/react @testing-library/jest-dom @testing-library/user-event

vite.config.ts

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

vitest.setup.ts

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

第一个组件测试

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

test('点击触发 onClick', async () => {
  const onClick = vi.fn();
  render(<Button onClick={onClick}>提交</Button>);

  await userEvent.click(screen.getByRole('button', { name: '提交' }));

  expect(onClick).toHaveBeenCalledOnce();
});

Testing Library 哲学:测行为不测实现

❌ 测实现细节(脆弱):

expect(wrapper.find('.btn-primary').length).toBe(1);

✅ 测用户能看见 / 能操作的(稳定):

expect(screen.getByRole('button', { name: '提交' })).toBeInTheDocument();

测试要重构友好:你改样式、改 className、改 DOM 结构,测试不应该挂——只要用户看到的行为没变。

查询优先级

1. getByRole         (最像真实用户)
2. getByLabelText    (表单字段)
3. getByPlaceholderText
4. getByText
5. getByDisplayValue
6. getByAltText / getByTitle
7. getByTestId       (最后手段)

90% 用 getByRolegetByTextgetByTestId 是兜底。

三类查询

  • getBy*:找不到抛错
  • queryBy*:找不到返回 null(用来断言不存在)
  • findBy*:异步,等到出现(默认 1 秒)
expect(screen.queryByText('错误')).not.toBeInTheDocument();   // 不存在
expect(await screen.findByText('加载完成')).toBeInTheDocument();   // 异步出现

测异步行为

test('加载用户', async () => {
  render(<UserPage id="1" />);
  expect(screen.getByText('加载中')).toBeInTheDocument();
  expect(await screen.findByText('张三')).toBeInTheDocument();
});

findBy* 会重试直到超时或找到——比 waitFor 更优先用。

模拟 fetch

import { vi } from 'vitest';

beforeEach(() => {
  vi.spyOn(global, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({ name: '张三' }),
  } as Response);
});

afterEach(() => vi.restoreAllMocks());

更复杂场景用 MSW(Mock Service Worker),它在网络层拦截,测试和生产代码用一样的 fetch 路径。

测 Hook

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

test('inc 加 1', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => result.current.inc());
  expect(result.current.count).toBe(1);
});

act 保证 state 更新 flush 完才断言。

测 Context / Provider

function renderWithAuth(ui: React.ReactElement, { user = null } = {}) {
  return render(
    <AuthProvider initialUser={user}>{ui}</AuthProvider>
  );
}

test('登录后显示用户名', () => {
  renderWithAuth(<Header />, { user: { name: '张三' } });
  expect(screen.getByText('张三')).toBeInTheDocument();
});

覆盖率不是目标

写测试是为了:

  • 防回归(改代码不破坏老功能)
  • 设计辅助(写测试发现 API 难用)

不是为了凑覆盖率数字。80% 业务逻辑 + 70% UI 关键路径比 100% 全覆盖更实在。

E2E:Playwright

npm init playwright@latest
test('用户能下单', async ({ page }) => {
  await page.goto('/products/1');
  await page.getByRole('button', { name: '加入购物车' }).click();
  await page.goto('/cart');
  await expect(page.getByText('1 件商品')).toBeVisible();
});

E2E 慢、脆弱,只覆盖最重要的几条路径:登录、下单、关键查询。其他用单测 / 组件测试。

CI 跑测

# .github/workflows/test.yml
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm test -- --run    # 一次性跑(不 watch)

→ 下一篇 性能调优