Jest 单元测试框架配置
目录
Jest 是 Facebook 开发的现代 JavaScript 测试框架,具有零配置、内置断言、模拟功能和代码覆盖率等特性,是现代 JavaScript 项目的首选测试解决方案。
Jest 简介
核心特性
- 零配置: 开箱即用,无需复杂配置
- 快照测试: 自动生成和比较组件输出快照
- 并行测试: 自动并行运行测试,提高效率
- 代码覆盖率: 内置代码覆盖率报告
- 模拟功能: 强大的 mock 和 spy 功能
- 监听模式: 文件变化时自动重新运行测试
适用场景
- 单元测试
- 集成测试
- 快照测试
- React/Vue 组件测试
- Node.js 应用测试
安装和基础配置
1. 安装 Jest
# 安装 Jest
npm install --save-dev jest
# 安装类型定义(TypeScript 项目)
npm install --save-dev @types/jest
2. 基础配置
创建 jest.config.js
文件:
module.exports = {
// 测试环境
testEnvironment: 'node', // 'node' | 'jsdom' | 'jsdom-sixteen'
// 测试文件匹配模式
testMatch: [
'**/tests/**/*.test.js',
'**/__tests__/**/*.js',
'**/?(*.)+(spec|test).js'
],
// 覆盖率配置
collectCoverage: true,
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/coverage/'
],
// 代码转换
transform: {
'^.+\\.js$': 'babel-jest',
},
// 测试设置文件
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
// 模块路径映射
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
3. 在 package.json 中配置
也可以在 package.json
中配置:
{
"jest": {
"testEnvironment": "node",
"collectCoverage": true,
"coverageDirectory": "coverage"
}
}
详细配置选项
测试环境配置
module.exports = {
// Node.js 环境(默认)
testEnvironment: 'node',
// 浏览器环境(需要 DOM API)
testEnvironment: 'jsdom',
// 自定义环境配置
testEnvironmentOptions: {
url: 'http://localhost',
userAgent: 'Custom User Agent',
},
// 不同文件使用不同环境
projects: [
{
displayName: 'Node',
testEnvironment: 'node',
testMatch: ['<rootDir>/tests/node/**/*.test.js'],
},
{
displayName: 'Browser',
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/tests/browser/**/*.test.js'],
},
],
};
覆盖率配置
module.exports = {
// 启用覆盖率收集
collectCoverage: true,
// 覆盖率输出目录
coverageDirectory: 'coverage',
// 覆盖率收集的文件模式
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/index.js',
'!**/node_modules/**',
],
// 忽略覆盖率的路径
coveragePathIgnorePatterns: [
'/node_modules/',
'/tests/',
'/dist/',
],
// 覆盖率报告格式
coverageReporters: [
'text', // 控制台输出
'lcov', // 生成 lcov.info 文件
'html', // HTML 报告
'json', // JSON 格式
'clover', // Clover XML 格式
],
// 覆盖率阈值
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'./src/utils/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
};
模块和路径配置
module.exports = {
// 模块路径映射
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@tests/(.*)$': '<rootDir>/tests/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
},
// 模块搜索路径
modulePaths: ['<rootDir>/src'],
// 模块文件扩展名
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx'],
// 解析器选项
resolver: undefined,
// 模块目录
moduleDirectories: ['node_modules', 'src'],
};
创建测试文件
1. 测试环境设置
创建 tests/setup.js
:
/**
* Jest 测试环境全局配置
*/
// 全局测试变量
global.testConfig = {
apiUrl: 'http://localhost:3000/api',
timeout: 5000,
};
// Mock 全局对象
global.console = {
...console,
// 在测试中隐藏某些日志
log: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
// 扩展 expect 匹配器
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
// 全局 beforeEach 和 afterEach
beforeEach(() => {
// 每个测试前的清理工作
jest.clearAllMocks();
});
afterEach(() => {
// 每个测试后的清理工作
jest.resetModules();
});
2. 基础测试示例
创建 tests/utils/helpers.test.js
:
import { greet, isEven, calculateSum } from '../../src/utils/helpers.js';
describe('Helper Functions', () => {
describe('greet', () => {
test('should return greeting with name', () => {
const result = greet('Alice');
expect(result).toBe('Hello, Alice!');
});
test('should handle empty name', () => {
expect(() => greet('')).toThrow('Name must be a non-empty string');
});
test('should handle non-string input', () => {
expect(() => greet(123)).toThrow('Name must be a non-empty string');
});
});
describe('isEven', () => {
test('should return true for even numbers', () => {
expect(isEven(2)).toBe(true);
expect(isEven(0)).toBe(true);
expect(isEven(-4)).toBe(true);
});
test('should return false for odd numbers', () => {
expect(isEven(1)).toBe(false);
expect(isEven(3)).toBe(false);
expect(isEven(-1)).toBe(false);
});
test('should throw error for non-number input', () => {
expect(() => isEven('2')).toThrow('Input must be a number');
});
});
describe('calculateSum', () => {
test('should calculate sum of array', () => {
expect(calculateSum([1, 2, 3, 4])).toBe(10);
});
test('should handle empty array', () => {
expect(calculateSum([])).toBe(0);
});
test('should handle negative numbers', () => {
expect(calculateSum([-1, -2, 3])).toBe(0);
});
});
});
3. 异步测试
// tests/async.test.js
import { fetchUserData, delayedFunction } from '../src/async-functions.js';
describe('Async Functions', () => {
// Promise 测试
test('should fetch user data', async () => {
const userData = await fetchUserData(1);
expect(userData).toHaveProperty('id', 1);
expect(userData).toHaveProperty('name');
});
// 错误处理测试
test('should handle fetch error', async () => {
await expect(fetchUserData(-1)).rejects.toThrow('User not found');
});
// 超时测试
test('should complete within timeout', async () => {
const start = Date.now();
await delayedFunction(100);
const duration = Date.now() - start;
expect(duration).toBeGreaterThanOrEqual(100);
expect(duration).toBeLessThan(150);
}, 200); // 设置测试超时时间
// 回调函数测试
test('should handle callback', (done) => {
const callback = (data) => {
try {
expect(data).toBe('success');
done();
} catch (error) {
done(error);
}
};
callbackFunction(callback);
});
});
Mock 和 Spy 功能
1. 基础 Mock
describe('Mock Examples', () => {
test('should mock function', () => {
const mockFn = jest.fn();
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
});
test('should mock with return value', () => {
const mockFn = jest.fn(() => 'mocked result');
const result = mockFn();
expect(result).toBe('mocked result');
});
test('should mock with different return values', () => {
const mockFn = jest.fn()
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call')
.mockReturnValue('default');
expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('second call');
expect(mockFn()).toBe('default');
});
});
2. 模块 Mock
// Mock 整个模块
jest.mock('../src/api-client.js', () => ({
fetchData: jest.fn(() => Promise.resolve({ data: 'mocked' })),
postData: jest.fn(() => Promise.resolve({ success: true })),
}));
// 部分 Mock
jest.mock('../src/utils.js', () => ({
...jest.requireActual('../src/utils.js'),
expensiveFunction: jest.fn(() => 'mocked result'),
}));
// 动态 Mock
describe('API Tests', () => {
beforeEach(() => {
jest.resetModules();
});
test('should use mocked API', async () => {
const { fetchData } = require('../src/api-client.js');
const result = await fetchData();
expect(result).toEqual({ data: 'mocked' });
expect(fetchData).toHaveBeenCalledTimes(1);
});
});
3. Spy 功能
import * as utils from '../src/utils.js';
describe('Spy Examples', () => {
test('should spy on module function', () => {
const spy = jest.spyOn(utils, 'formatDate');
spy.mockReturnValue('2023-12-22');
const result = utils.formatDate(new Date());
expect(spy).toHaveBeenCalledTimes(1);
expect(result).toBe('2023-12-22');
spy.mockRestore(); // 恢复原始函数
});
test('should spy on object method', () => {
const obj = {
method: jest.fn(() => 'original'),
};
const spy = jest.spyOn(obj, 'method');
obj.method();
expect(spy).toHaveBeenCalled();
});
});
添加测试脚本
在 package.json
中添加测试脚本:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --coverage --watchAll=false --passWithNoTests",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"test:update": "jest --updateSnapshot"
}
}
脚本说明
test
: 运行所有测试test:watch
: 监听模式,文件变化时自动运行test:coverage
: 生成覆盖率报告test:ci
: CI/CD 环境专用,不监听文件变化test:debug
: 调试模式test:update
: 更新快照测试
高级配置
1. 多项目配置
module.exports = {
projects: [
{
displayName: 'Unit Tests',
testMatch: ['<rootDir>/tests/unit/**/*.test.js'],
testEnvironment: 'node',
},
{
displayName: 'Integration Tests',
testMatch: ['<rootDir>/tests/integration/**/*.test.js'],
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/integration/setup.js'],
},
{
displayName: 'E2E Tests',
runner: '@jest-runner/electron',
testMatch: ['<rootDir>/tests/e2e/**/*.test.js'],
},
],
};
2. 自定义匹配器
// tests/matchers.js
export const toBeValidEmail = (received) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(received);
return {
message: () => `expected ${received} ${pass ? 'not ' : ''}to be a valid email`,
pass,
};
};
// jest.config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
};
// tests/setup.js
import { toBeValidEmail } from './matchers.js';
expect.extend({
toBeValidEmail,
});
3. 自定义转换器
// custom-transformer.js
module.exports = {
process(src, filename) {
// 自定义文件转换逻辑
return `module.exports = ${JSON.stringify(src)};`;
},
};
// jest.config.js
module.exports = {
transform: {
'\\.txt$': '<rootDir>/custom-transformer.js',
'\\.js$': 'babel-jest',
},
};
常见问题和解决方案
1. ES6 模块问题
问题: Jest 不支持 ES6 模块语法
解决: 配置 Babel 转换
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
],
};
// jest.config.js
module.exports = {
transform: {
'^.+\\.js$': 'babel-jest',
},
};
2. 异步测试超时
问题: 异步测试超时失败
解决: 设置适当的超时时间
// 全局超时设置
// jest.config.js
module.exports = {
testTimeout: 10000, // 10 秒
};
// 单个测试超时
test('long running test', async () => {
// 测试代码
}, 15000); // 15 秒
3. Mock 清理问题
问题: Mock 状态在测试间相互影响
解决: 正确清理 Mock
// jest.config.js
module.exports = {
clearMocks: true, // 自动清理 mock 调用
resetMocks: true, // 自动重置 mock 实现
restoreMocks: true, // 自动恢复 spy
};
// 或在测试中手动清理
beforeEach(() => {
jest.clearAllMocks();
});
性能优化
1. 并行执行优化
// jest.config.js
module.exports = {
maxWorkers: '50%', // 使用 50% 的 CPU 核心
maxConcurrency: 5, // 最大并发测试数
workerIdleMemoryLimit: 0.2, // 内存限制
};
2. 缓存配置
module.exports = {
cache: true,
cacheDirectory: '<rootDir>/.jest-cache',
};
3. 忽略不必要的文件
module.exports = {
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/build/',
],
watchPathIgnorePatterns: [
'/node_modules/',
'/.git/',
],
};
与 CI/CD 集成
GitHub Actions 配置
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
完善的测试配置确保代码质量和可靠性,是现代 JavaScript 开发的重要组成部分。
阿里云百炼大模型
9折优惠 + 所有模型各百万免费Token →