2026-02-15•ToolBox Team
前端单元测试与集成测试完全指南
🔧 返回工具箱 | Back to Tools
浏览所有工具 | View All Toolstestingfrontendjestquality-assurance
前端单元测试与集成测试完全指南
"不测试的代码是遗留代码"。一个拥有完善测试的项目,不仅代码质量更高,重构也更安心。本文将带你从零开始构建前端测试体系。
1. 为什么需要测试?
现实:没有测试的后果
修改了一行代码
↓
自己浏览器中测试通过
↓
推送到生产环境
↓
用户报告:某个功能在特定场景下崩溃
↓
紧急回滚 + 客户投诉 + 声誉受损
解决方案:自动化测试
修改了一行代码
↓
运行测试套件(自动覆盖所有场景)
↓
测试失败 → 代码修复 → 测试通过
↓
有信心地推送到生产
2. 测试金字塔
端到端测试 (E2E) ▲ 耗时
/ \ │
集成测试 集成测试 │
/ \ / \ │
单元 单元 单元 单元 ▼ 快速
测试 测试 测试 测试
70%: 单元测试
20%: 集成测试
10%: E2E 测试
3. Jest 基础设置
安装与配置
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
保存 jest.config.js:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/**/*.test.{js,jsx}',
'!src/index.js'
]
};
Package.json 中添加脚本
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
4. 单元测试
测试函数逻辑
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// math.test.js
import { add, subtract } from './math';
describe('数学函数', () => {
test('add 返回两数之和', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('subtract 返回两数之差', () => {
expect(subtract(5, 2)).toBe(3);
expect(subtract(1, 5)).toBe(-4);
});
});
测试对象和数组
// user.js
export class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getProfile() {
return {
name: this.name,
email: this.email
};
}
}
// user.test.js
import { User } from './user';
describe('User 类', () => {
test('创建用户并获取信息', () => {
const user = new User('Alice', 'alice@example.com');
expect(user.getProfile()).toEqual({
name: 'Alice',
email: 'alice@example.com'
});
});
});
测试异步代码
// api.js
export async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// api.test.js
import { fetchUser } from './api';
describe('API 函数', () => {
test('fetchUser 返回用户数据', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({
id: 1,
name: 'Alice',
email: 'alice@example.com'
})
})
);
const user = await fetchUser(1);
expect(user).toEqual({
id: 1,
name: 'Alice',
email: 'alice@example.com'
});
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
});
5. React 组件测试
测试简单组件
// Button.jsx
export function Button({ label, onClick, disabled }) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button 组件', () => {
test('正确渲染按钮标签', () => {
render(<Button label="点击我" />);
const button = screen.getByRole('button', { name: /点击我/i });
expect(button).toBeInTheDocument();
});
test('可以禁用按钮', () => {
render(<Button label="点击我" disabled={true} />);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
test('点击时调用 onClick 事件', () => {
const handleClick = jest.fn();
render(<Button label="点击我" onClick={handleClick} />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
测试带状态的组件
// Counter.jsx
import { useState } from 'react';
export function Counter({ initialValue = 0 }) {
const [count, setCount] = useState(initialValue);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>加</button>
<button onClick={() => setCount(count - 1)}>减</button>
</div>
);
}
// Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
describe('Counter 组件', () => {
test('初始值为 0', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
test('点击"加"按钮增加计数', () => {
render(<Counter />);
const addButton = screen.getByRole('button', { name: /加/i });
fireEvent.click(addButton);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('点击"减"按钮减少计数', () => {
render(<Counter />);
const subtractButton = screen.getByRole('button', { name: /减/i });
fireEvent.click(subtractButton);
expect(screen.getByText('Count: -1')).toBeInTheDocument();
});
});
测试 Hook
// useCounter.js
import { useState } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
return {
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
reset: () => setCount(initialValue)
};
}
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter Hook', () => {
test('返回初始值', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
});
test('increment 增加计数', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('reset 重置计数', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});
6. Mock 与 Stub
Mock 函数和模块
// userService.js
export async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// component.test.js
import * as userService from './userService';
import { getUserInfo } from './component';
jest.mock('./userService');
test('当 API 失败时显示错误', async () => {
userService.getUser.mockRejectedValue(
new Error('API 失败')
);
const component = render(<UserInfo userId={1} />);
await waitFor(() => {
expect(screen.getByText(/API 失败/i)).toBeInTheDocument();
});
});
Mock API 响应
// api.test.js
import axios from 'axios';
jest.mock('axios');
describe('API 调用', () => {
test('处理成功响应', async () => {
axios.get.mockResolvedValue({
data: { id: 1, name: 'Alice' }
});
const data = await fetchUser(1);
expect(data).toEqual({ id: 1, name: 'Alice' });
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});
test('处理错误响应', async () => {
axios.get.mockRejectedValue(
new Error('网络错误')
);
await expect(fetchUser(1)).rejects.toThrow('网络错误');
});
});
7. 覆盖率与缺陷检测
生成覆盖率报告
npm run test:coverage
输出示例:
TOTAL | 85.2% | 78.5% | 92.1% | 84.8%
- Statements:语句覆盖率(85.2%)
- Branches:分支覆盖率(78.5%)
- Functions:函数覆盖率(92.1%)
- Lines:行覆盖率(84.8%)
目标:至少 80% 覆盖率。
8. 测试最佳实践
✅ 应该做
// 1. 描述清晰的测试名称
test('当输入无效时,验证函数应返回错误', () => {
// ...
});
// 2. 遵循 Arrange-Act-Assert 模式
test('登录成功后应重定向到首页', () => {
// Arrange
const user = new User('alice', 'password123');
// Act
const result = user.login();
// Assert
expect(result.redirectTo).toBe('/home');
});
// 3. 一个测试只验证一个逻辑
test('add 函数应返回两数之和', () => {
expect(add(2, 3)).toBe(5);
});
// 4. 使用有意义的断言
expect(user.age).toBeGreaterThanOrEqual(18);
expect(email).toMatch(/^[\w-]+@([\w-]+\.)+[\w-]{2,4}$/);
expect(items).toHaveLength(3);
❌ 不应该做
// 不:多个逻辑在一个测试中
test('用户功能', () => {
const user = new User('alice');
expect(user.name).toBe('alice');
user.setAge(25);
expect(user.age).toBe(25);
user.email = 'alice@example.com';
expect(user.email).toBe('alice@example.com');
});
// 不:模糊的断言
expect(result).toBeTruthy(); // 应该:expect(result).toBe(true)
// 不:过度 Mock
jest.spyOn(console, 'log'); // 通常不必要
相关工具推荐: