如果用一个词来形容我摸索 Jest 配置的过程,就是“试错”。刚开始我照着模板抄了一份jest.config.js,能跑起来就谢天谢地,但换个项目就崩,报错信息五花八门。后来我才意识到,配置之所以反复出问题,是因为我没理解 Jest 自己的工作流程:它先扫描哪些文件,再按什么规则转换这些文件,最后在什么环境里执行用例。这三个环节没理顺,配置就永远像在碰运气。
Jest 本身只是一个测试运行器,负责发现测试文件、执行用例、汇总结果。它并不直接处理 JSX、TypeScript、路径别名、CSS 导入、DOM 环境这些工程化问题,所以需要通过transform、moduleNameMapper、setupFiles等一系列配置项,把项目里的现实情况告诉它。这篇内容专门讲 Jest 配置,适合刚开始接触 Jest、或者写了几个用例但配置全靠复制粘贴的同学。读完你会知道每个配置项为什么存在、怎么改、改了会影响什么,而不是继续当一个“配置搬运工”。
1. 理解 Jest 配置前必须想清楚的三件事
1.1 Jest 的边界:它只负责跑测试,不负责编译项目
很多新手(包括以前的我)会默认 Jest 什么都能干:你是 TS 项目?它能跑。你用 JSX?它能跑。你引入了 CSS?它也应该能跑。实际上 Jest 只做三件事:找测试文件、执行用例、汇总统计。
至于怎么把 TypeScript 转成 JavaScript、怎么处理 JSX、怎么识别 CSS Modules,这些通通不在 Jest 的核心能力范围内。Jest 提供的是扩展机制,也就是配置项,让使用者告诉它怎么处理这些情况。我把这种关系理解为:Jest 是一个只会跑步的运动员,赛道怎么铺、跑鞋怎么换,都是教练(配置文件)的事。
所以当你遇到“配置不生效”的问题,优先问自己一个问题:这个报错是 Jest 找不到文件,还是不知道怎么执行文件?这两个方向的排查路径完全不同。
1.2 配置的两大核心任务:找得到文件、跑得起来
我给 Jest 配置拆过一条很清晰的主线,所有配置项基本都可以归到两类:
- 发现任务:Jest 要知道去哪里找测试文件、哪些文件不算测试文件、用什么路径规则来匹配。对应配置项是
roots、testMatch、testPathIgnorePatterns。 - 执行任务:Jest 要知道如何把测试文件转换成自己能执行的格式,以及用什么样的环境来执行。对应配置项是
transform、moduleNameMapper、testEnvironment、setupFiles。
记住这个分类之后,你看到配置项就不会迷茫了。比如transformIgnorePatterns为什么默认忽略node_modules,是因为 Jest 认为依赖里的代码已经是被处理的产物,不需要经过 Babel 再转一次。这个逻辑放在“执行任务”下理解就很通顺。
1.3 配置项粒度:别一上来就追求大而全
网上很多模板式的 Jest 配置能写六七十行,看起来非常专业,但大部分字段你的项目根本用不上。我个人的建议是:先搞一个最小可用配置,跑通之后再加字段。加一个字段前先想清楚,这个字段解决的是哪个报错或哪种场景,而不是因为它“看起来很标准”。
Jest 配置同样遵循“最小改动、可解释、可回滚”的原则。配置文件的每一行都应该是被某个实际问题逼出来的,而不是为了显得项目规范。
2. 从零起步:第一个能运行的 jest.config.js
2.1 最简配置骨架到底需要几个字段
我们先安装 Jest:
npm install --save-dev jest然后在package.json里加一个测试脚本:
{ "scripts": { "test": "jest" } }如果项目里没有任何配置,Jest 也能工作。它默认会去匹配这些文件:
__tests__目录下的所有.js、.jsx、.ts、.tsx文件- 文件名以
.test.js、.spec.js结尾的文件
换句话说,零配置也能跑最简单的测试用例。但真实项目不可能这么顺利,你很快就会遇到 JSX 没法解析、路径别名不生效、引入 CSS 直接报错之类的问题。所以最简配置骨架通常会包含这几项:
module.exports = { testEnvironment: 'node', testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'], clearMocks: true, };testEnvironment决定用例跑在什么环境里,testMatch定义测试文件的范围,clearMocks保证每个用例之间 mock 状态不串。这三项可以算是大多数普通项目的起手式。
2.2 testEnvironment 决定你的用例运行在什么“世界”里
Jest 支持两种内置环境,默认值是node。如果你测试的是 Node 模块、工具函数,或者不依赖 DOM 的纯逻辑,用node就对了,它启动快、内存占用小。
但如果你测试的是 React 组件、DOM 操作,或者代码里出现了window、document这些浏览器全局对象,就必须切换环境:
testEnvironment: 'jsdom'简单理解:jsdom在 Node 进程里模拟了一个浏览器 DOM 环境,让测试代码能访问到window、document等对象。
这里有两个常见的坑。第一,jsdom不是真正的浏览器,它没有布局引擎,getBoundingClientRect返回的值经常是 0,有些原生浏览器 API 它也不实现,比如canvas的getContext需要额外 mock。第二,如果你所有用例都用jsdom,但一半用例根本不碰 DOM,那启动速度和内存占用都会白白增加。我的做法是先按代码的类型来选环境,后面再拆多配置。
当前testEnvironment的可选值除了内置的node和jsdom,还可以装第三方的happy-dom或@happy-dom/jest-environment。happy-dom启动速度比jsdom快,API 兼容性也不错,如果你的项目只是轻度依赖 DOM,可以试试它。不过我在实际项目中遇到过一次选择器行为不一致的问题,后来还是回了jsdom,稳定优先。
2.3 testMatch 与 roots:Jest 去哪里找测试文件
Jest 默认会用一套 glob 模式去匹配测试文件,这套模式基本够用,但很多团队有自己的目录约定,比如测试文件统一放在test/目录下,或者文件名统一叫*.test.js,这时候就需要显式配置:
testMatch: ['<rootDir>/test/**/*.test.js']roots则用来限定 Jest 扫描的根目录。默认是项目根目录,如果你只在某几个目录里放测试文件,可以显式缩小范围,减少扫描时间:
roots: ['<rootDir>/src', '<rootDir>/test']还有一个容易被忽略但很重要的字段是testPathIgnorePatterns,它用来排除不需要被当成测试文件的路径。默认已经忽略了node_modules,但如果你项目里有dist、build之类的构建产物目录,建议手动加上:
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/build/']提示:千万不要把所有目录都塞进
testMatch,否则 Jest 会尝试把一些非测试文件当成用例执行,轻则报错,重则误跑一些有副作用的模块。
3. 模块解析与文件转换:让 Jest 看懂项目代码
3.1 transform 的作用与 babel-jest 的配置
只要项目里出现了 JSX、TS、ES Module 这些语法,Jest 就必须借助transform把代码转成自己能执行的格式。
babel-jest是 Jest 自带且默认使用的转换器,它会读取项目根目录的babel.config.js(或.babelrc)来做转换。一个常见的配置是这样的:
module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-react', { runtime: 'automatic' }], ], };@babel/preset-env负责把 ES Module、箭头函数等新语法转成当前 Node 版本能识别的写法,@babel/preset-react负责把 JSX 转成函数调用。这里runtime: 'automatic'的意思是不用每次 import React,Babel 会自动注入 runtime 函数,新项目推荐使用。
如果项目是纯 JavaScript 且没用到任何新语法,理论上可以完全不配置transform,Jest 能直接跑。但一旦用了import或者 JSX,就必须让 Babel 参与。这里的核心逻辑是:先 Babel 转,再交给 Jest 执行。
3.2 ts-jest 与 TypeScript 项目的选择
TypeScript 项目里最常见的方案有两种:ts-jest和babel-jest + @babel/preset-typescript。
ts-jest直接集成 TypeScript 编译器,会做类型检查,配置也很简单:
transform: { '^.+\\.tsx?$': 'ts-jest', }优点是配置少、开箱即用,缺点也很明显:慢。如果你的项目测试文件多,跑一次全量测试可能要等很久。
如果你对测试速度敏感,可以使用 Babel 方案:
module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript'], ['@babel/preset-react', { runtime: 'automatic' }], ], };Babel 方案只做语法转换,不做类型检查,所以测试速度会快很多,代价是测试阶段不会暴露类型错误。我的习惯是用 Babel 方案跑测试,类型检查单独用一条命令比如tsc --noEmit在 CI 阶段跑,两边分工。
另一个容易踩的坑是:用ts-jest时,tsconfig.json里的module和moduleResolution会影响 Jest 对模块的解析结果。如果遇到“模块解析失败、找不到某个导入”,先检查tsconfig.json里的配置,再去看moduleNameMapper是否给@/这类别名做了映射。
3.3 moduleNameMapper:别名和静态资源的统一出口
项目里最常见的两个“Jest 看不懂的东西”是路径别名和静态资源。
路径别名比如:
import Button from '@/components/Button';这里的@指向src,但 Jest 的模块解析并不知道这个规则。如果不配置,它会按照node_modules的查找逻辑去找@/components/Button,结果必然是找不到模块。解决方式:
moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }静态资源问题也很类似。项目里经常会有:
import './index.css'; import logo from './logo.png';Node 环境没法解析这些文件,直接执行会报Unexpected token之类的错误。处理方式是映射成一个 mock 对象或者代理:
moduleNameMapper: { '\\.(css|less|scss)$': 'identity-obj-proxy', '\\.(png|jpg|gif|svg)$': '<rootDir>/test/__mocks__/fileMock.js', }identity-obj-proxy是处理 CSS Modules 的常用工具,它能让styles.foo这种访问返回字符串foo。如果不想多装依赖,也可以自己写一个极简 fileMock:
module.exports = {};所有图片、字体文件在测试里都被当成了一个空对象,不影响用例逻辑。
注意:
moduleNameMapper是正则匹配,不是字符串替换。写映射规则时一定要用^和$限定边界,否则很容易误伤一些合法的模块路径。
3.4 transformIgnorePatterns 和 node_modules 的坑
Jest 默认不会转换node_modules里的代码,这个行为对应的配置项是:
transformIgnorePatterns: ['/node_modules/']但现代前端项目里很多依赖直接发布 ESM 源码,比如一些只提供.mjs入口的库。此时 Jest 在解析import语句时会直接报错,因为 Node 环境的 CommonJS 解析机制不能直接执行 ESM 语法。
解决办法是把这些库加到白名单,让 Jest 对它们也做 Babel 转换:
transformIgnorePatterns: ['/node_modules/(?!(library-a|library-b)/)']这个正则的意思是:除了library-a和library-b,其他node_modules下的代码都不转换。
这里有一个实际经验:尽量缩小白名单范围,不要把整个node_modules都放进转换范围,否则测试速度会肉眼可见地变慢。而且有些库的 ESM 版本依赖浏览器环境,强行转换后反而会在 Node 环境里报错。如果某个依赖始终折腾不通,先看看它有没有 CommonJS 入口文件,比如dist/index.cjs.js,有的话可以通过moduleNameMapper强制指向它,比折腾transformIgnorePatterns更省心。
4. setup 文件与用例生命周期配置
4.1 setupFiles 与 setupFilesAfterEnv 的差异
Jest 提供了两个容易混淆的配置项:setupFiles和setupFilesAfterEnv。它们在生命周期里的执行时机完全不同。
setupFiles在测试框架安装之前执行,时机非常早。适合放一些纯 polyfill 和全局 mock,比如:
module.exports = { setupFiles: ['<rootDir>/test/setup.js'], };setupFilesAfterEnv在测试框架安装完成、global对象和expectAPI 都准备好之后执行。这里适合扩展断言库、注册全局辅助函数,最常见的用法是配合测试库的匹配器:
module.exports = { setupFilesAfterEnv: ['<rootDir>/test/setupAfterEnv.js'], };test/setup.js里常见的例子是 mockmatchMedia,因为很多组件库会在代码里直接调用它:
Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation((query) => ({ matches: false, media: query, addEventListener: jest.fn(), removeEventListener: jest.fn(), })), });test/setupAfterEnv.js里常见的例子是引入断言扩展:
import '@testing-library/jest-dom';提示:很多人在 setup 文件里加了一大堆 mock,加多了之后容易互相干扰。一个比较务实的原则是:
setupFiles里只放能让用例不崩溃的浏览器 API mock,不放断言扩展;setupFilesAfterEnv里只放和测试断言相关的内容。
4.2 全局 mock、清理策略与 watch 交互
Jest 的 mock 清理策略是三兄弟:clearMocks、resetMocks、restoreMocks。它们的区别经常让人记混,我整理了一个速查对照:
| 配置项 | 行为 | 典型场景 |
|---|---|---|
clearMocks: true | 每个用例前清除 mock 的调用记录(calls、results),但保留 mock 实现 | 最常见,避免用例之间调用次数互相干扰 |
resetMocks: true | 每个用例前把 mock 重置为未定义实现的状态 | 需要保证每个用例从完全干净的 mock 开始 |
restoreMocks: true | 每个用例前把jest.spyOn包装的函数恢复为原始实现 | 当你用spyOn监听真实方法,又担心用例泄漏时 |
我的习惯是:
clearMocks: true, restoreMocks: true,这样既清掉了调用记录,又把 spy 状态还原了,基本能覆盖日常工作流。resetMocks 用到的场景相对少一些,因为如果 mock 实现是在beforeEach里动态设置的,resetMocks也能配合上。
还有一个跟 watch 模式相关的小技巧:在 watch 模式下跑测试时,Jest 会缓存文件转换结果,如果你改了babel.config.js或者moduleNameMapper但感觉没生效,可以先跑一次:
npx jest --clearCache这个命令是 Jest 官方提供的,用来清除转换缓存。很多时候“配置没生效”其实是缓存导致的,并不是配置写错了。
5. 按项目场景拆分配置:从单文件到多环境并存
5.1 单配置文件 vs 多环境拆分
一个常见的尴尬是:项目里既有纯函数工具库的测试,又有 React 组件的 DOM 测试。前者用node环境更快,后者必须用jsdom,两份测试放在同一个 Jest 进程里,只能用同一个testEnvironment,不是不够快,就是跑不起来。
Jest 提供了projects字段,可以同时跑多份配置。每份配置里的displayName会在终端显示出来,方便区分:
module.exports = { projects: [ { displayName: 'node', testEnvironment: 'node', testMatch: ['<rootDir>/test/unit/**/*.test.js'], }, { displayName: 'dom', testEnvironment: 'jsdom', testMatch: ['<rootDir>/test/dom/**/*.test.js'], }, ], };用projects的一个坑是:每个子配置都是独立的 Jest 实例,都需要重新声明transform、moduleNameMapper这些公共配置。如果项目里公共配置很多,可以考虑放一个jest.config.base.js,在子配置里引用它,避免重复维护。
如果你的项目没那么复杂,我其实更推荐先别拆,直接把testEnvironment设为jsdom,等测试数量上来了、确实感受到启动速度影响之后,再引入projects。配置拆分的价值在于清晰和性能,如果项目规模很小,拆分的维护成本反而成了负担。
5.2 常见场景组合参考
不同项目类型的 Jest 配置差异挺大,我列几个比较典型的场景组合,方便你对照自己的项目。
React + TypeScript + Vite 项目的精简配置:
module.exports = { testEnvironment: 'jsdom', transform: { '^.+\\.tsx?$': 'babel-jest', }, moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', '\\.(css|less)$': 'identity-obj-proxy', }, setupFilesAfterEnv: ['<rootDir>/test/setupAfterEnv.ts'], };纯 Node 工具库的配置:
module.exports = { testEnvironment: 'node', testMatch: ['<rootDir>/test/**/*.test.js'], collectCoverageFrom: ['src/**/*.js'], };依赖了一定 DOM API 但主体是逻辑的项目,可以先用 jsdom:
module.exports = { testEnvironment: 'jsdom', setupFiles: ['<rootDir>/test/setup.js'], };还有一个容易忽略的字段是moduleFileExtensions,它控制 Jest 在解析模块时按什么顺序寻找文件:
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']默认值已经包含了常见扩展名,一般不需要改。但如果项目里混用了.mjs和.cjs,或者你依赖了 alias 指向无扩展名的文件,可能就需要调整排列顺序。
6. 常见问题与排查实战
6.1 案例:npm test 直接报 SyntaxError
报错信息类似:
SyntaxError: Cannot use import statement outside a module这个报错说明有一个文件的import语法没被转换。优先级最高的排查顺序是:
- 看清楚报错的文件路径,是项目源码还是
node_modules里的依赖。 - 如果是项目源码:检查 Babel 配置是否正确登记了对应语法,比如
@babel/preset-env和@babel/preset-typescript是否齐全。 - 如果是
node_modules里的依赖:检查transformIgnorePatterns是否需要把这个包加入白名单。 - 如果项目里有
.mjs文件:确认 Jest 的transform是否覆盖到了.mjs后缀。
大多数情况问题出在transformIgnorePatterns上。前端依赖生态越来越复杂,发布 ESM 的库越来越多,这个报错的频率也在变高。
6.2 案例:import 一个 CSS 文件直接报错
报错信息通常是:
Cannot find module './index.css' from 'xxx.tsx'这个就是典型的静态资源解析问题。大部分框架项目不需要在 Jest 里真实加载 CSS,只需要让它假装存在。处理方式前面已经说过,用moduleNameMapper映射到一个 mock 对象:
moduleNameMapper: { '\\.(css|less|scss)$': 'identity-obj-proxy', }如果没有装identity-obj-proxy,也可以用一个简单的代理对象:
moduleNameMapper: { '\\.(css|less|scss)$': '<rootDir>/test/styleMock.js', }styleMock.js内容:
module.exports = new Proxy({}, { get: (_, key) => key, });这样styles.container返回'container',大多数断言场景都够用了。
6.3 案例:window/document 明明存在却报 undefined
这类问题一般分两种:
第一种是你没有切到jsdom环境,代码里出现的window或document是 undefined。解决方法是把testEnvironment改成jsdom。
第二种是你已经在jsdom环境里了,但某个浏览器 API 是jsdom没实现的,比如window.matchMedia、canvas.getContext、ResizeObserver。这些需要自己在setupFiles里 mock 掉,或者安装对应的 mock 库,比如jest-canvas-mock。
排查这类问题的小技巧是:在用例里直接打印typeof window.xxx,先确认这个 API 到底是“没定义”还是“有定义但行为不对”,再决定要不要 mock。
6.4 排查技巧速查表
我把日常遇到最多的 Jest 配置问题整理成一张速查表:
| 现象 | 最可能原因 | 优先检查项 |
|---|---|---|
Cannot use import statement outside a module | 文件没被 transform 或依赖是 ESM | transform、transformIgnorePatterns |
Cannot find module路径里有@/ | 别名没映射 | moduleNameMapper |
| 引入 CSS/图片报错 | 静态资源没处理 | moduleNameMapper里的静态资源映射 |
window/documentis undefined | 环境跑在 node | testEnvironment |
| 某个浏览器 API 不可用 | jsdom 未实现该 API | setupFiles里 mock |
| 改名或改配置后行为不变 | Jest 缓存 | npx jest --clearCache |
| 测试文件没找到 | testMatch 范围不对 | testMatch、testPathIgnorePatterns |
| 多 package 项目重复跑用例 | rootDir 没对准 | roots、projects的 rootDir |
排查配置问题我有一个习惯,就是永远从最短的路径试起。先确认文件是否被 Jest 匹配到,再确认文件是否经过 transform,最后确认环境是否合适。按这个顺序走,大部分配置类报错都能在几分钟内定位,而不是在搜索引擎里无尽翻滚。
我个人配置 Jest 这几年最值钱的一个习惯是:新项目一律先把一个空用例跑通,再逐步叠加配置。每加一个配置项之前都问自己“现在跑不过去是因为什么”,而不是“别人都这么配我也这么配”。Jest 的配置体系不算复杂,它的报错也足够直白,真正让人头疼的往往是项目里杂乱的工程化细节。把“先找文件、再转文件、再执行”这条主线记在脑子里,遇到问题时按顺序排查,配置这件事就会从玄学变成普通的技术问题。