测什么
| 类型 | 用 |
|---|---|
| 纯函数 / 工具 | Vitest 单测 |
| Hooks | Vitest + @testing-library/react 的 renderHook |
| 组件 | 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% 用 getByRole 和 getByText。getByTestId 是兜底。
三类查询
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)
→ 下一篇 性能调优