名称: jest-configuration 用户不可调用: false 描述: 当需要 Jest 配置、设置文件、模块解析和项目组织以优化测试环境时使用。 允许工具: [读取, 写入, 编辑, Bash, Glob, Grep]
Jest 配置
掌握 Jest 配置、设置文件、模块解析和项目组织,以创建优化的测试环境。此技能涵盖了为现代 JavaScript 和 TypeScript 项目配置 Jest 的所有方面,从基本设置到高级的多项目配置。
安装和设置
基本安装
# npm
npm install --save-dev jest
# yarn
yarn add --dev jest
# pnpm
pnpm add -D jest
TypeScript 支持
npm install --save-dev @types/jest ts-jest
React 测试库
npm install --save-dev @testing-library/react @testing-library/jest-dom
配置文件
jest.config.js(推荐)
/** @type {import('jest').Config} */
module.exports = {
// 测试环境
testEnvironment: 'node', // 或 'jsdom' 用于类似浏览器的环境
// 测试的根目录
roots: ['<rootDir>/src'],
// 要考虑的文件扩展名
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
// 测试匹配模式
testMatch: [
'**/__tests__/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[jt]s?(x)'
],
// 测试前转换文件
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest'
},
// 覆盖率配置
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,jsx,ts,tsx}',
'!src/**/__tests__/**'
],
// 覆盖率阈值
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// 设置文件
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// 导入的模块名称映射
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js'
},
// 在测试之间清除模拟
clearMocks: true,
// 在测试之间恢复模拟
restoreMocks: true,
// 详细输出
verbose: true
};
TypeScript 配置(jest.config.ts)
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': ['ts-jest', {
tsconfig: {
esModuleInterop: true,
allowSyntheticDefaultImports: true
}
}]
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/__tests__/**'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
export default config;
Package.json 配置
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --maxWorkers=2"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node"
}
}
设置文件
jest.setup.js
// 全局测试设置
import '@testing-library/jest-dom';
// 设置全局测试超时
jest.setTimeout(10000);
// 模拟环境变量
process.env.NODE_ENV = 'test';
process.env.API_URL = 'http://localhost:3000';
// 全局 before/after 钩子
beforeAll(() => {
// 在所有测试之前运行的设置代码
console.log('开始测试套件');
});
afterAll(() => {
// 在所有测试之后运行的清理代码
console.log('测试套件完成');
});
// 模拟控制台方法以减少噪音
global.console = {
...console,
error: jest.fn(),
warning: jest.fn()
};
// 自定义匹配器
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
};
}
}
});
React 测试设置
import '@testing-library/jest-dom';
import { configure } from '@testing-library/react';
// 配置测试库
configure({ testIdAttribute: 'data-testid' });
// 模拟 window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
});
// 模拟 IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
};
模块解析
路径映射
// jest.config.js
module.exports = {
moduleNameMapper: {
// 别名映射
'^@/(.*)$': '<rootDir>/src/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^@hooks/(.*)$': '<rootDir>/src/hooks/$1',
'^@services/(.*)$': '<rootDir>/src/services/$1',
// 样式模拟
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
// 资产模拟
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
'\\.(woff|woff2|eot|ttf|otf)$': '<rootDir>/__mocks__/fileMock.js'
},
// 模块目录
modulePaths: ['<rootDir>/src'],
// 忽略的模块路径
modulePathIgnorePatterns: [
'<rootDir>/dist/',
'<rootDir>/build/',
'<rootDir>/coverage/'
]
};
文件模拟
// __mocks__/fileMock.js
module.exports = 'test-file-stub';
// __mocks__/styleMock.js
module.exports = {};
多项目配置
Monorepo 设置
// jest.config.js
module.exports = {
projects: [
{
displayName: 'client',
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/packages/client/**/*.test.{js,jsx,ts,tsx}'],
setupFilesAfterEnv: ['<rootDir>/packages/client/jest.setup.js']
},
{
displayName: 'server',
testEnvironment: 'node',
testMatch: ['<rootDir>/packages/server/**/*.test.{js,ts}'],
setupFilesAfterEnv: ['<rootDir>/packages/server/jest.setup.js']
},
{
displayName: 'shared',
testEnvironment: 'node',
testMatch: ['<rootDir>/packages/shared/**/*.test.{js,ts}']
}
],
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'packages/*/src/**/*.{js,jsx,ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**'
]
};
项目特定配置
// packages/client/jest.config.js
module.exports = {
displayName: 'client',
preset: '../../jest.preset.js',
testEnvironment: 'jsdom',
transform: {
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@babel/preset-react'] }]
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
}
};
环境配置
自定义测试环境
// custom-environment.js
const NodeEnvironment = require('jest-environment-node').default;
class CustomEnvironment extends NodeEnvironment {
constructor(config, context) {
super(config, context);
this.testPath = context.testPath;
}
async setup() {
await super.setup();
// 自定义设置逻辑
this.global.testEnvironmentSetup = true;
}
async teardown() {
// 自定义清理逻辑
delete this.global.testEnvironmentSetup;
await super.teardown();
}
getVmContext() {
return super.getVmContext();
}
}
module.exports = CustomEnvironment;
// jest.config.js
module.exports = {
testEnvironment: './custom-environment.js'
};
转换配置
Babel 转换
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
'@babel/preset-react'
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-runtime'
]
};
自定义转换器
// custom-transformer.js
const { createTransformer } = require('babel-jest');
module.exports = createTransformer({
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript'
],
plugins: ['babel-plugin-transform-import-meta']
});
最佳实践
- 使用 TypeScript 配置文件以确保类型安全 - 利用 TypeScript 配置文件在编译时捕获配置错误
- 在
__tests__目录中组织测试 - 将测试靠近源文件,以便更好地发现 - 设置适当的覆盖率阈值 - 定义现实的覆盖率目标,平衡全面性和可维护性
- 使用设置文件进行全局配置 - 集中常见设置逻辑,避免在测试文件中重复
- 配置模块名称映射以使导入更清晰 - 使用路径别名,使测试导入更易读和可维护
- 分离环境特定的配置 - 为 Node 和浏览器环境使用不同的配置
- 在测试之间清除模拟 - 通过自动重置模拟来防止测试污染
- 为 monorepo 设置使用项目 - 利用多项目配置实现更好的组织
- 配置适当的超时 - 为异步操作设置现实的超时,以防止误报失败
- 在开发过程中使用详细输出 - 启用详细日志记录,以帮助调试测试失败
常见陷阱
- 忘记安装必需的依赖 - 缺少 @types/jest 或测试库会导致隐晦的错误
- 模块解析路径不正确 - 错误配置的 moduleNameMapper 导致模块未找到错误
- 未在测试之间清除模拟 - 共享的模拟状态导致测试不稳定和假阳性
- 覆盖率阈值过于严格 - 不现实的覆盖率目标会阻碍测试并减慢开发
- 缺少转换配置 - 文件未转换导致测试中出现语法错误
- 全局和本地配置冲突 - Package.json 配置意外覆盖 jest.config.js
- 未正确配置测试环境 - 使用错误的环境(node 与 jsdom)导致未定义错误
- 忽略 setupFilesAfterEnv - 缺少全局设置导致每个测试文件中重复的样板代码
- 未处理 CSS/资产导入 - 未模拟的样式导入在 Node 环境中破坏测试
- 测试匹配模式不正确 - 由于模式不匹配,未发现测试
何时使用此技能
- 在新项目中从头设置 Jest
- 从另一个测试框架迁移到 Jest
- 为 TypeScript 项目配置 Jest
- 为 monorepos 设置测试基础设施
- 为 CI/CD 管道优化 Jest 配置
- 调试测试中的模块解析问题
- 配置自定义测试环境
- 设置路径别名以使导入更清晰
- 为特殊文件类型实现自定义转换器
- 为团队建立覆盖率要求