Jest 最佳实践 #
测试原则 #
FIRST 原则 #
text
┌─────────────────────────────────────────────────────────────┐
│ FIRST 原则 │
├─────────────────────────────────────────────────────────────┤
│ F - Fast(快速) 测试应该快速执行 │
│ I - Independent(独立)测试之间不应有依赖 │
│ R - Repeatable(可重复)任何环境都能得到相同结果 │
│ S - Self-validating(自验证)测试结果应该是明确的 │
│ T - Timely(及时)测试应该与代码同步编写 │
└─────────────────────────────────────────────────────────────┘
测试金字塔 #
text
/\
/ \
/ E2E\ - 少量端到端测试
/------\
/ 集成测试 \ - 适量集成测试
/----------\
/ 单元测试 \ - 大量单元测试
/--------------\
测试组织 #
文件结构 #
text
src/
├── components/
│ ├── Button/
│ │ ├── Button.jsx
│ │ ├── Button.styles.js
│ │ ├── Button.test.jsx
│ │ └── index.js
│ └── Input/
│ ├── Input.jsx
│ └── Input.test.jsx
├── utils/
│ ├── math.js
│ └── math.test.js
└── __tests__/
├── integration/
│ └── user-flow.test.js
└── e2e/
└── checkout.test.js
测试文件命名 #
javascript
// 单元测试
math.test.js
math.spec.js
// 集成测试
api.integration.test.js
user-flow.integration.test.js
// E2E 测试
checkout.e2e.test.js
测试结构 #
javascript
describe('Component/Module name', () => {
describe('method/feature name', () => {
describe('scenario', () => {
test('should expected behavior', () => {
// Arrange
// Act
// Assert
});
});
});
});
命名规范 #
测试描述 #
javascript
// ❌ 不好的命名
test('test1', () => {});
test('works', () => {});
// ✅ 好的命名
test('should return sum of two numbers', () => {});
test('should throw error when input is invalid', () => {});
test('should render button with correct text', () => {});
使用一致的命名模式 #
javascript
// 模式:should [expected behavior] when [condition]
test('should return true when user is logged in', () => {});
test('should return false when password is incorrect', () => {});
test('should throw error when email is invalid', () => {});
测试策略 #
单元测试 #
javascript
// 测试纯函数
describe('formatDate', () => {
test('formats date correctly', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('2024-01-15');
});
test('handles invalid input', () => {
expect(() => formatDate(null)).toThrow();
});
});
组件测试 #
javascript
// 测试组件行为
describe('Button', () => {
test('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
test('handles click event', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalled();
});
});
API 测试 #
javascript
// 测试 API 端点
describe('POST /api/users', () => {
test('creates user with valid data', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
name: 'John',
email: 'john@example.com',
});
});
});
测试隔离 #
避免共享状态 #
javascript
// ❌ 不好的做法
describe('shared state', () => {
let counter = 0;
test('test 1', () => {
counter++;
expect(counter).toBe(1);
});
test('test 2', () => {
// 依赖 test 1 的状态
expect(counter).toBe(1);
});
});
// ✅ 好的做法
describe('isolated state', () => {
let counter;
beforeEach(() => {
counter = 0;
});
test('test 1', () => {
counter++;
expect(counter).toBe(1);
});
test('test 2', () => {
expect(counter).toBe(0);
});
});
清理 Mock #
javascript
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
Mock 最佳实践 #
只 Mock 必要的依赖 #
javascript
// ❌ 不好的做法 - Mock 所有依赖
jest.mock('lodash');
jest.mock('axios');
jest.mock('moment');
// ✅ 好的做法 - 只 Mock 外部依赖
jest.mock('axios');
使用简单的 Mock 实现 #
javascript
// ❌ 不好的做法
jest.mock('./api', () => ({
fetch: jest.fn(() => {
return new Promise((resolve) => {
setTimeout(() => resolve({ data: 'test' }), 100);
});
}),
}));
// ✅ 好的做法
jest.mock('./api', () => ({
fetch: jest.fn().mockResolvedValue({ data: 'test' }),
}));
断言最佳实践 #
使用语义化断言 #
javascript
// ❌ 不好的做法
expect(user !== undefined).toBe(true);
expect(items.length === 3).toBe(true);
// ✅ 好的做法
expect(user).toBeDefined();
expect(items).toHaveLength(3);
一个测试一个断言点 #
javascript
// ❌ 不好的做法
test('user operations', () => {
const user = createUser();
expect(user.id).toBeDefined();
expect(user.name).toBe('John');
expect(user.email).toBe('john@example.com');
});
// ✅ 好的做法
test('creates user with id', () => {
const user = createUser();
expect(user.id).toBeDefined();
});
test('creates user with correct name', () => {
const user = createUser();
expect(user.name).toBe('John');
});
异步测试 #
正确处理异步 #
javascript
// ❌ 不好的做法
test('bad async', () => {
fetchData().then(data => {
expect(data).toBe('expected');
});
});
// ✅ 好的做法
test('good async', async () => {
const data = await fetchData();
expect(data).toBe('expected');
});
设置合理的超时 #
javascript
test('slow operation', async () => {
const result = await slowOperation();
expect(result).toBeDefined();
}, 10000); // 10 秒超时
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
with:
files: ./coverage/lcov.info
package.json scripts #
json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"test:update": "jest --updateSnapshot"
}
}
测试覆盖率 #
设置合理的阈值 #
javascript
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
};
关键代码更高覆盖率 #
javascript
coverageThreshold: {
'./src/core/': {
branches: 90,
functions: 90,
lines: 90,
},
'./src/utils/': {
branches: 80,
functions: 80,
lines: 80,
},
},
性能优化 #
并行执行 #
javascript
// jest.config.js
module.exports = {
maxWorkers: '50%',
};
增量测试 #
bash
# 只运行修改的测试
jest --onlyChanged
# 只运行与 git 变更相关的测试
jest --changedSince=main
缓存 #
javascript
// jest.config.js
module.exports = {
cache: true,
cacheDirectory: '/tmp/jest_cache',
};
测试文档 #
使用注释说明复杂测试 #
javascript
test('handles complex discount calculation', () => {
// 场景:用户购买 3 件商品,其中 1 件打折
// 预期:总价 = 原价 * 2 + 折扣价 * 1
const items = [
{ price: 100, discount: 0 },
{ price: 100, discount: 0 },
{ price: 100, discount: 0.2 },
];
const total = calculateTotal(items);
expect(total).toBe(280);
});
常见反模式 #
1. 测试实现细节 #
javascript
// ❌ 不好的做法
expect(wrapper.vm.count).toBe(1);
// ✅ 好的做法
expect(wrapper.text()).toContain('1');
2. 过度 Mock #
javascript
// ❌ 不好的做法
jest.mock('lodash');
jest.mock('./utils');
// ✅ 好的做法
jest.mock('axios');
3. 测试依赖顺序 #
javascript
// ❌ 不好的做法
test('test 1', () => {
sharedState = 'value';
});
test('test 2', () => {
expect(sharedState).toBe('value');
});
// ✅ 好的做法
beforeEach(() => {
sharedState = 'value';
});
最佳实践清单 #
text
□ 测试原则
├── 遵循 FIRST 原则
├── 遵循测试金字塔
└── 测试用户行为
□ 测试组织
├── 合理的文件结构
├── 一致的命名规范
└── 清晰的测试结构
□ 测试隔离
├── 避免共享状态
├── 清理 Mock
└── 独立的测试
□ Mock 使用
├── 只 Mock 必要的依赖
├── 使用简单的 Mock 实现
└── 正确清理 Mock
□ 断言
├── 使用语义化断言
├── 一个测试一个断言点
└── 清晰的错误消息
□ 异步测试
├── 正确处理异步
├── 设置合理的超时
└── 使用 async/await
□ CI/CD
├── 自动化测试
├── 覆盖率报告
└── 测试分片
□ 性能
├── 并行执行
├── 增量测试
└── 缓存优化
总结 #
编写好的测试需要:
- 理解测试目的 - 测试是为了保证代码质量,不是为了覆盖率
- 关注用户行为 - 测试用户看到和交互的内容
- 保持简单 - 简单的测试更容易维护
- 持续改进 - 定期回顾和优化测试代码
好的测试是项目成功的关键,投入时间学习和实践测试技术是值得的!
最后更新:2026-03-28