Skip to main content

单元测试

单元测试是投入产出比最高的测试类型。前端单元测试主要针对:

  • 工具函数(utils)
  • 自定义 Hooks
  • 公共组件
  • 状态管理 store
  • 业务核心逻辑

一、推荐工具

工具适用特点
Jest通用生态最丰富,配置简单
VitestVite 项目启动快,与 Vite 配置共享
React Testing LibraryReact 组件测用户行为,不测实现
Vue Test UtilsVue 组件官方推荐
jest-domDOM 断言增强 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,
},
},
},
};

五、避坑指南

  1. 不要测实现细节:不要断言 state.x === 1,应该断言用户看到的结果
  2. 避免测试私有函数:通过公开 API 间接覆盖
  3. mock 不要过度:mock 多了测试就失去意义
  4. 异步用 waitFor:不要用 setTimeout 等异步
  5. 不要查 DOM 结构:用 getByRole / getByText 而不是 querySelector
  6. 覆盖率追求 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 测用户视角,重构更友好。