单元测试
单元测试是投入产出比最高的测试类型。前端单元测试主要针对:
- 工具函数(utils)
- 自定义 Hooks
- 公共组件
- 状态管理 store
- 业务核心逻辑
一、推荐工具
| 工具 | 适用 | 特点 |
|---|---|---|
| Jest | 通用 | 生态最丰富,配置简单 |
| Vitest | Vite 项目 | 启动快,与 Vite 配置共享 |
| React Testing Library | React 组件 | 测用户行为,不测实现 |
| Vue Test Utils | Vue 组件 | 官方推荐 |
| jest-dom | DOM 断言 | 增强 toBeInTheDocument 等 API |
新项目 Vite + Vitest + Testing Library 是当前最优组合。
二、基础示例(Jest)
1. 工具函数
// utils/add.ts
export const add = (a: number, b: number) => a + b;
// utils/add.test.ts
import { describe, it, expect } from "vitest";
import { add } from "./add";
describe("add", () => {
it("两个正数相加", () => {
expect(add(1, 2)).toBe(3);
});
it("处理负数", () => {
expect(add(-1, 1)).toBe(0);
});
it("处理 0", () => {
expect(add(0, 0)).toBe(0);
});
});
2. 自定义 Hook
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";
describe("useCounter", () => {
it("初始值", () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
});
it("increment", () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
});
3. React 组件
import { render, screen, fireEvent } from "@testing-library/react";
import { Counter } from "./Counter";
describe("<Counter />", () => {
it("渲染初始值", () => {
render(<Counter initial={5} />);
expect(screen.getByText("count: 5")).toBeInTheDocument();
});
it("点击 +1", () => {
render(<Counter initial={0} />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByText("count: 1")).toBeInTheDocument();
});
});
三、Mock
1. 函数 mock
import { vi } from "vitest";
const fetchUser = vi.fn().mockResolvedValue({ id: 1, name: "Lucas" });
it("调用接口", async () => {
await getUserInfo();
expect(fetchUser).toHaveBeenCalledTimes(1);
});
2. 模块 mock
vi.mock("./api", () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1 }),
}));
3. 时间 mock
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-01"));
// ... 测试代码
vi.useRealTimers();
四、覆盖率达到 80% 的实践
# jest
jest --coverage
# vitest
vitest run --coverage
vitest.config.ts:
export default {
test: {
coverage: {
provider: "v8",
reporter: ["text", "html"],
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.test.{ts,tsx}", "src/main.tsx"],
thresholds: {
lines: 80,
functions: 80,
branches: 60,
},
},
},
};
五、避坑指南
- 不要测实现细节:不要断言
state.x === 1,应该断言用户看到的结果 - 避免测试私有函数:通过公开 API 间接覆盖
- mock 不要过度:mock 多了测试就失去意义
- 异步用 waitFor:不要用
setTimeout等异步 - 不要查 DOM 结构:用
getByRole/getByText而不是querySelector - 覆盖率追求 80% 而非 100%:剩下的 20% 大多是边界/异常,成本极高
六、CI 集成
# GitHub Actions
- name: 单测
run: pnpm test -- --run --coverage
- name: 检查覆盖率
run: pnpm test -- --run --coverage --coverageThreshold='{"lines":80}'
七、面试高频
- Q:单元测试和 E2E 测试怎么选? A:单元测试测函数和组件、跑得快、定位准;E2E 测完整链路、跑得慢、脆弱。单元为主,E2E 兜底核心链路。
- Q:什么是 TDD? A:Test-Driven Development,先写失败测试 → 写最小实现让它通过 → 重构。三步循环。
- Q:React Testing Library 为什么不推荐 enzyme? A:RTL 主张测行为不测实现。enzyme 直接操作组件实例,组件重构(hooks 改 class)测试就挂了;RTL 测用户视角,重构更友好。