Jest 最佳实践 #
测试原则 #
FIRST 原则 #
text
┌─────────────────────────────────────────────────────────────┐
│ FIRST 原则 │
├─────────────────────────────────────────────────────────────┤
│ F - Fast 测试应该快速执行 │
│ I - Independent 测试应该相互独立 │
│ R - Repeatable 测试应该可重复 │
│ S - Self-validating 测试应该自我验证 │
│ T - Timely 测试应该及时编写 │
└─────────────────────────────────────────────────────────────┘
测试金字塔 #
text
/\
/ \ E2E 测试(少量)
/────\
/ \ 集成测试(适量)
/────────\
/ \ 单元测试(大量)
/────────────\
测试组织 #
文件结构 #
text
project/
├── src/
│ ├── components/
│ │ ├── Button.jsx
│ │ └── Button.test.jsx
│ ├── utils/
│ │ ├── math.js
│ │ └── math.test.js
│ └── services/
│ ├── userService.js
│ └── userService.test.js
├── __tests__/
│ ├── integration/
│ │ └── api.test.js
│ └── e2e/
│ └── userFlow.test.js
└── __mocks__/
└── axios.js
测试文件命名 #
text
# 单元测试
Component.test.jsx
module.test.js
service.test.ts
# 集成测试
integration.test.js
api.test.js
# E2E 测试
e2e.test.js
userFlow.test.js
测试命名规范 #
描述性命名 #
javascript
// ✅ 好的命名
describe('UserService', () => {
describe('createUser', () => {
test('should create user with valid data', () => {});
test('should throw error when email is missing', () => {});
test('should throw error when email is invalid', () => {});
});
});
// ❌ 不好的命名
describe('test', () => {
test('test1', () => {});
test('test2', () => {});
});
中文命名(团队约定) #
javascript
describe('用户服务', () => {
describe('创建用户', () => {
test('应该成功创建有效用户', () => {});
test('邮箱缺失时应该抛出错误', () => {});
test('邮箱格式错误时应该抛出错误', () => {});
});
});
测试结构 #
AAA 模式 #
javascript
test('should calculate total price', () => {
// Arrange - 准备
const cart = new Cart();
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 },
];
// Act - 执行
const total = cart.calculateTotal(items);
// Assert - 断言
expect(total).toBe(250);
});
Given-When-Then 模式 #
javascript
test('should apply discount to total', () => {
// Given - 给定
const cart = new Cart();
cart.addItem({ price: 100 });
// When - 当
cart.applyDiscount(0.1);
// Then - 那么
expect(cart.total).toBe(90);
});
测试隔离 #
独立测试 #
javascript
// ✅ 好的做法 - 每个测试独立
describe('User', () => {
let user;
beforeEach(() => {
user = new User('John');
});
test('should have correct name', () => {
expect(user.name).toBe('John');
});
test('should update name', () => {
user.setName('Jane');
expect(user.name).toBe('Jane');
});
});
// ❌ 不好的做法 - 测试依赖
describe('User', () => {
let user = new User('John');
test('test 1', () => {
user.setName('Jane');
});
test('test 2', () => {
// 依赖 test 1 的状态
expect(user.name).toBe('Jane');
});
});
清理资源 #
javascript
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
database.close();
});
Mock 最佳实践 #
Mock 边界 #
javascript
// ✅ Mock 外部依赖
jest.mock('axios');
jest.mock('./api');
// ❌ 不要 Mock 被测试的代码
jest.mock('./calculator'); // 错误!
清理 Mock #
javascript
afterEach(() => {
jest.restoreAllMocks();
});
使用 spyOn #
javascript
test('spy on method', () => {
const spy = jest.spyOn(obj, 'method');
obj.method('arg');
expect(spy).toHaveBeenCalledWith('arg');
spy.mockRestore();
});
断言最佳实践 #
使用语义化断言 #
javascript
// ✅ 好的做法
expect(user).toBeDefined();
expect(items).toHaveLength(3);
expect(error).toThrow();
// ❌ 不好的做法
expect(user !== undefined).toBe(true);
expect(items.length === 3).toBe(true);
单一断言 #
javascript
// ✅ 好的做法 - 每个测试一个断言点
test('should calculate total', () => {
const total = calculateTotal(items);
expect(total).toBe(150);
});
test('should apply discount', () => {
const discounted = applyDiscount(100, 0.1);
expect(discounted).toBe(90);
});
// ❌ 不好的做法 - 多个断言
test('cart operations', () => {
expect(cart.items).toHaveLength(1);
expect(cart.total).toBe(100);
expect(cart.discount).toBe(0);
});
异步测试最佳实践 #
使用 async/await #
javascript
// ✅ 好的做法
test('fetches user', async () => {
const user = await fetchUser(1);
expect(user).toBeDefined();
});
// ❌ 不好的做法
test('fetches user', () => {
fetchUser(1).then(user => {
expect(user).toBeDefined();
});
});
错误处理 #
javascript
// ✅ 好的做法
test('handles error', async () => {
await expect(fetchUser(-1)).rejects.toThrow();
});
// ❌ 不好的做法
test('handles error', async () => {
try {
await fetchUser(-1);
} catch (error) {
expect(error).toBeDefined();
}
});
测试覆盖策略 #
优先级 #
text
1. 核心业务逻辑 - 100% 覆盖
2. 工具函数 - 90%+ 覆盖
3. UI 组件 - 70%+ 覆盖
4. 配置文件 - 可选覆盖
覆盖率目标 #
javascript
// jest.config.js
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
'./src/core/': {
branches: 90,
functions: 90,
lines: 90,
},
},
CI/CD 集成 #
GitHub Actions #
yaml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage --ci
- name: Upload coverage
uses: codecov/codecov-action@v3
GitLab CI #
yaml
test:
image: node:18
script:
- npm ci
- npm test -- --coverage --ci
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
Pre-commit Hook #
json
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm test"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"jest --bail --findRelatedTests"
]
}
}
性能优化 #
并行执行 #
javascript
// jest.config.js
module.exports = {
maxWorkers: '50%',
};
缓存 #
javascript
module.exports = {
cache: true,
cacheDirectory: '/tmp/jest_cache',
};
增量测试 #
bash
# 只运行修改的测试
jest --onlyChanged
# 基于分支变更
jest --changedSince=main
常见陷阱 #
1. 测试实现细节 #
javascript
// ❌ 不好的做法
test('internal state', () => {
expect(component.state('count')).toBe(0);
});
// ✅ 好的做法
test('user behavior', () => {
expect(screen.getByText('0')).toBeInTheDocument();
});
2. 过度 Mock #
javascript
// ❌ 不好的做法 - Mock 一切
jest.mock('./utils');
jest.mock('./services');
jest.mock('./components');
// ✅ 好的做法 - 只 Mock 外部依赖
jest.mock('axios');
3. 忽略边界情况 #
javascript
// ✅ 测试边界情况
test.each([
[0, 0],
[1, 1],
[-1, -1],
[100, 100],
])('handles %i', (input, expected) => {
expect(process(input)).toBe(expected);
});
test('handles null', () => {
expect(() => process(null)).toThrow();
});
test('handles undefined', () => {
expect(() => process(undefined)).toThrow();
});
测试文档 #
测试描述文档化 #
javascript
/**
* UserService 测试套件
*
* 测试范围:
* - 用户创建
* - 用户查询
* - 用户更新
* - 用户删除
*
* 边界情况:
* - 无效输入
* - 重复邮箱
* - 权限检查
*/
describe('UserService', () => {
// 测试代码
});
总结 #
测试检查清单 #
text
□ 测试命名清晰描述行为
□ 测试相互独立
□ 使用 AAA 模式组织测试
□ Mock 外部依赖
□ 测试边界情况
□ 测试错误处理
□ 保持测试简洁
□ 定期运行测试
□ CI 中强制运行
□ 保持覆盖率目标
恭喜你完成了 Jest 测试框架的学习!现在你已经掌握了从入门到专家的完整知识体系,可以开始在实际项目中应用这些技能了!
最后更新:2026-03-28