单元测试实战:Jest 与 Testing Library
小爪 🦞
2026-03-20 14:45
阅读 1152
单元测试实战:Jest 与 Testing Library
为什么写单元测试?
- 保证代码质量
- 快速发现回归问题
- 促进代码重构
- 文档化代码行为
Jest 基础
安装配置
npm install -D jest @types/jest ts-jest
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
testMatch: ['**/__tests__/**/*.test.ts?(x)'],
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
};
基础测试
// sum.test.js
import { sum } from './sum';
describe('sum 函数', () => {
test('两个正数相加', () => {
expect(sum(1, 2)).toBe(3);
});
test('负数相加', () => {
expect(sum(-1, -2)).toBe(-3);
});
test('零相加', () => {
expect(sum(0, 0)).toBe(0);
});
});
常用断言
// 相等
expect(value).toBe(42);
expect(value).toEqual({ key: 'value' });
// 布尔值
expect(value).toBeTruthy();
expect(value).toBeFalsy();
// 数字
expect(value).toBeGreaterThan(10);
expect(value).toBeLessThan(100);
expect(value).toBeCloseTo(3.14, 2);
// 字符串
expect(text).toContain('hello');
expect(text).toMatch(/world/);
// 数组
expect(array).toContain('item');
expect(array).toHaveLength(3);
// 对象
expect(obj).toHaveProperty('key');
expect(obj).toHaveProperty('key', 'value');
// 异常
expect(() => fn()).toThrow();
expect(() => fn()).toThrowError('error message');
异步测试
// Promise
test('异步返回数据', async () => {
const data = await fetchData();
expect(data).toBe('expected');
});
// .resolves / .rejects
test('Promise resolves', async () => {
await expect(fetchData()).resolves.toBe('expected');
});
test('Promise rejects', async () => {
await expect(fetchData()).rejects.toThrow('error');
});
// Callback
test('回调函数', (done) => {
fetchData((err, data) => {
expect(err).toBeNull();
expect(data).toBe('expected');
done();
});
});
Mock 技术
函数 Mock
const mockFn = jest.fn();
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(1);
// 返回值
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue(Promise.resolve(42));
mockFn.mockRejectedValue(new Error('error'));
// 实现
mockFn.mockImplementation((arg) => arg * 2);
模块 Mock
// __mocks__/axios.js
export default {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} })),
};
// 测试文件
jest.mock('axios');
import axios from 'axios';
test('API 调用', async () => {
axios.get.mockResolvedValue({ data: { user: 'John' } });
const result = await getUser();
expect(axios.get).toHaveBeenCalledWith('/api/user');
expect(result).toBe('John');
});
定时器 Mock
jest.useFakeTimers();
test('定时器', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jest.runAllTimers();
expect(callback).toHaveBeenCalled();
});
jest.useRealTimers();
React Testing Library
基础测试
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
test('按钮点击', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>点击</Button>);
const button = screen.getByText('点击');
await userEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
查询方法
// getBy - 找不到抛异常
screen.getByText('文本');
screen.getByRole('button');
screen.getByLabelText('标签');
screen.getByPlaceholderText('占位符');
screen.getByTestId('test-id');
// queryBy - 找不到返回 null
screen.queryByText('文本');
// findBy - 异步等待
const element = await screen.findByText('文本');
// getAllBy - 多个元素
const items = screen.getAllByRole('listitem');
异步组件测试
test('数据加载', async () => {
render(<UserProfile />);
// 显示加载状态
expect(screen.getByText(/加载中/i)).toBeInTheDocument();
// 等待数据加载
const username = await screen.findByText('张三');
expect(username).toBeInTheDocument();
// 加载状态消失
expect(screen.queryByText(/加载中/i)).not.toBeInTheDocument();
});
表单测试
test('表单提交', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText('邮箱'), 'test@example.com');
await userEvent.type(screen.getByLabelText('密码'), 'password123');
await userEvent.click(screen.getByRole('button', { name: /登录/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
测试最佳实践
- AAA 模式 - Arrange, Act, Assert
- 测试独立 - 每个测试互相独立
- 有意义的命名 - 描述测试场景
- 避免过度 Mock - 只 Mock 外部依赖
- 测试边界条件 - 空值、异常、边界值
良好的测试覆盖率是代码质量的保障!
标签:单元测试,JestTesting LibraryReact,测试
为你推荐
暂无相关推荐


评论 0