在 webpack 项目中使用 Jest:配置迁移、静态资源 Mock 与模块解析全指南
【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest
Jest 完全可以与使用 webpack 管理资源、样式与编译流程的项目协同工作——本指南将完整演示如何把一份典型的 webpack 配置(loader、asset 规则、resolve别名与目录)逐项翻译为等价的 Jest 配置,并深入讲解moduleNameMapper、moduleDirectories、moduleFileExtensions、modulePaths与自定义transform的底层实现。读完本文,你将掌握在 webpack 工程中落地 Jest、优雅处理样式与图片等静态资源、以及正确配置模块查找路径的完整实战方案。
本文基于 Jest 30 官方文档 Webpack.md(与 docs/Webpack.md 内容一致)整理并扩充。
为什么 webpack 项目集成 Jest 会“特殊”
webpack 与其他工具相比之所以给测试带来独特挑战,在于它深度集成了应用本身:它负责管理样式表、图片和字体等资源,并支撑起庞大的"编译到 JavaScript"语言与工具生态。而 Jest 默认的模块解析与文件加载机制并不认识 CSS、图片等资源,因此需要一套显式的配置来"翻译"webpack 的能力。
好消息是:Jest 的大部分配置项都能与 webpack 的对应能力一一映射。核心思路是:
- 用
transform处理需要编译的 JavaScript/TypeScript 代码(默认走babel-jest); - 用
moduleNameMapper把样式、图片等资源文件替换为 Mock 模块; - 用
moduleDirectories、moduleFileExtensions、modulePaths复刻 webpack 的模块查找逻辑; - 用
moduleNameMapper的正则映射复刻 webpack 的resolve.alias。
一个典型的 webpack 配置示例
下面是一份常见的 webpack 配置,它同时处理 JS/JSX 编译、CSS 样式、内联图片与字体资源,并配置了路径别名与自定义查找目录:
module.exports = { module: { rules: [ { test: /\.jsx?$/, exclude: ['node_modules'], use: ['babel-loader'], }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, { test: /\.gif$/, type: 'asset/inline', }, { test: /\.(ttf|eot|svg)$/, type: 'asset/resource', }, ], }, resolve: { alias: { config$: './configs/app-config.js', react: './vendor/react-master', }, extensions: ['.js', '.jsx'], modules: [ 'node_modules', 'bower_components', 'shared', '/shared/vendor/modules', ], }, };这份配置中的每个部分,在 Jest 中都有对应的落点:
| webpack 配置 | 职责 | Jest 对应配置 |
|---|---|---|
module.rules(babel-loader) | 编译 JS/JSX | transform+babel-jest |
module.rules(css/asset) | 加载样式与资源 | moduleNameMapper或transform |
resolve.extensions | 可省略扩展名的文件后缀 | moduleFileExtensions |
resolve.modules | 模块查找目录 | moduleDirectories+modulePaths |
resolve.alias | 模块路径别名 | moduleNameMapper(正则映射) |
如果项目中的 JavaScript 文件由 Babel 转换,可安装babel-jest插件来启用 Babel 支持(参见 GettingStarted.md);非 Babel 的 JavaScript 转换则可用 Jest 的transform配置项处理。
babel-jest 到底做了什么
babel-jest是 Jest 官方提供的 Babel 转换器,实现位于 packages/babel-jest/src/index.ts。从源码可见几个关键事实:
- 它通过
createTransformer工厂创建符合 Jesttransform接口的转换器,实现了process/processAsync(同步/异步编译)与getCacheKey/getCacheKeyAsync(缓存键计算); - 默认会把
babel-preset-jest追加进 Babel 的 presets(presets: [...(inputOptions.presets ?? []), ...(excludeJestPreset === true ? [] : [jestPresetPath])]),该 preset 负责jest.mock等调用的提升(hoisting)——因此文档特别提示:如果显式声明excludeJestPreset: true,会破坏jest.mock的提升机制; - 生成的缓存键(cache key)会综合 Babel 配置、源码内容、相对 rootDir 的路径、
instrument标志、NODE_ENV/BABEL_ENV与 Node 版本等计算(见 getCacheKeyFromConfig)。这就是"修改了.babelrc之后需要jest --clearCache"的底层原因——Babel 配置变化会影响缓存键,但旧缓存可能未失效。
babel-jest的 README(packages/babel-jest/README.md)给出了最简洁的接入方式:安装babel-jest后它会自动用 Babel 编译 JavaScript;只有当你需要同时使用多个代码预处理器时,才需要显式在transform中声明它:
"transform": { "\\.[jt]sx?$": "babel-jest" },还可以向babel-jest传递额外的 Babel 选项,例如:
"transform": { "\\.[jt]sx?$": ["babel-jest", { "extends": "./babel.config.js", "plugins": ["babel-plugin-transform-import-meta"] }] }处理静态资源(Handling Static Assets)
webpack 能把 CSS、图片、字体等资源打包进应用,但这些文件对单元测试没有实际价值,因此标准做法是把它们 Mock 掉。通过moduleNameMapper,可以把匹配到的资源扩展名替换成指定的 Mock 模块:
module.exports = { moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js', '\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js', }, };对应的两个 Mock 文件内容极简:
module.exports = {};module.exports = 'test-file-stub';即:样式模块返回空对象(因为测试中不关心样式),图片/字体等文件模块返回一个占位字符串'test-file-stub'。你可以根据 webpack 配置实际处理的文件类型,自由调整这里的正则表达式。
Mocking CSS Modules
如果项目使用 CSS Modules,直接返回空对象会导致styles.foobar为undefined。更优雅的方案是使用 ES6 Proxy 库identity-obj-proxy来 Mock CSS Modules,安装方式:
npm install --save-dev identity-obj-proxy然后在moduleNameMapper中把样式映射到该代理库:
module.exports = { moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js', '\\.(css|less)$': 'identity-obj-proxy', }, };这样样式对象上的所有 className 查询都会原样返回,例如styles.foobar === 'foobar'。这对 React 的 Snapshot Testing(快照测试)非常有用——组件快照中可以稳定看到类名字符串。
用自定义 transformer 处理资源
如果moduleNameMapper无法满足需求,可以使用 Jest 的transform配置项来指定资源的转换方式。例如下面这个 transformer 返回文件 basename,使require('logo.jpg')返回'logo':
const path = require('path'); module.exports = { process(sourceText, sourcePath, options) { return { code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`, }; }, };module.exports = { moduleNameMapper: { '\\.(css|less)$': 'identity-obj-proxy', }, transform: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/fileTransformer.js', }, };注意:此例中process返回的是{ code }形式的对象,这也是当前 Jest 版本中转换器标准的返回值形态(babel-jest的process同样返回{code, map},见 packages/babel-jest/src/index.ts)。
:::tip 保留默认 babel-jest 如果要在额外代码预处理器之外继续使用默认的babel-jest,请务必显式包含它,否则 JS/JSX 代码将不再走 Babel 编译:
"transform": { "\\.[jt]sx?$": "babel-jest", "\\.css$": "some-css-transformer" }:::
配置 Jest 找到我们的文件
处理完"如何转换文件",还需要告诉 Jest"到哪里找文件"。webpack 的resolve.modules与resolve.extensions,在 Jest 中分别有直接的对应项:moduleDirectories与moduleFileExtensions。
module.exports = { moduleFileExtensions: ['js', 'jsx'], moduleDirectories: ['node_modules', 'bower_components', 'shared'], moduleNameMapper: { '\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js', '\\.(gif|ttf|eot|svg)$': '<rootDir>/__mocks__/fileMock.js', }, };moduleFileExtensions:模块可省略扩展名的后缀数组,对应 webpack 的resolve.extensions;moduleDirectories:从"发起 require 的模块所在位置"逐级向上递归搜索的目录名数组(见 Descriptions.ts),对应 webpack 的resolve.modules;moduleNameMapper:资源 Mock 继续生效。
:::note 关于<rootDir><rootDir>是 Jest 的特殊令牌,运行时会被替换为项目根目录。大多数情况下它就是package.json所在目录,除非你在配置中指定了自定义的rootDir。在 jest-config 的归一化逻辑中,moduleNameMapper的值会经过_replaceRootDirTags把<rootDir>替换为真实的options.rootDir(见 normalize.ts),modulePaths/roots等数组项也会先replaceRootDirInPath再解析为绝对路径(见 normalize.ts)。 :::
modulePaths:对应 webpack 的 resolve.roots
webpack 的resolve.roots(设置NODE_PATH的替代方案)在 Jest 中的对应项是modulePaths:
module.exports = { modulePaths: ['/shared/vendor/modules'], moduleFileExtensions: ['js', 'jsx'], moduleDirectories: ['node_modules', 'bower_components', 'shared'], moduleNameMapper: { '\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js', '\\.(gif|ttf|eot|svg)$': '<rootDir>/__mocks__/fileMock.js', }, };modulePaths用于追加额外的绝对查找路径(如'/shared/vendor/modules'),从源码看它会与roots一起被解析为基于rootDir的绝对路径(见 normalize.ts)。
用 moduleNameMapper 复刻 resolve.alias
最后是 webpack 的resolve.alias。Jest 同样通过moduleNameMapper的正则映射来复刻:key 是匹配 import/require 路径的正则,value 是目标路径。
module.exports = { modulePaths: ['/shared/vendor/modules'], moduleFileExtensions: ['js', 'jsx'], moduleDirectories: ['node_modules', 'bower_components', 'shared'], moduleNameMapper: { '\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js', '\\.(gif|ttf|eot|svg)$': '<rootDir>/__mocks__/fileMock.js', '^react(.*)$': '<rootDir>/vendor/react-master$1', '^config$': '<rootDir>/configs/app-config.js', }, };两个关键点:
'^react(.*)$': '<rootDir>/vendor/react-master$1'中,$1是正则捕获组引用,可把react及其子路径(如react-dom)一并重定向到 vendor 目录,等价于 webpack 配置中的react: './vendor/react-master';'^config$': '<rootDir>/configs/app-config.js'精确匹配config模块,等价于 webpack 的config$: './configs/app-config.js'。
注意正则写法:webpack 用config$的$表示"以 config 结尾",Jest 的moduleNameMapper则用^config$表示"完整匹配 config 字符串",二者效果一致但语法习惯不同。
从 jest-config 源码看,moduleNameMapper的值支持"正则 → 模块名或模块名数组"的映射,并且每个 value 都会做<rootDir>标签替换(见 normalize.ts),官方描述为"从正则表达式到模块名或模块名数组的映射,用于用单个模块 stub 掉资源"(见 Descriptions.ts)。
让 Babel 与 Jest 协同:preset 与缓存
在babel-jest之外,如果项目使用 Babel 编译 ES 新语法,还需要安装@babel/preset-env:
npm install --save-dev @babel/preset-env然后配置 Babel:
{ "presets": ["@babel/preset-env"] }:::tip 清理缓存 Jest 会缓存文件以加速测试执行。如果你更新了.babelrc而 Jest 表现异常,尝试运行jest --clearCache清空缓存。 :::
其原理可参考上文对babel-jest缓存键的分析:缓存键由 Babel 配置、源码、环境变量等共同决定(getCacheKeyFromConfig),当.babelrc变更但缓存未失效时,就可能出现结果与预期不符的情况。
动态 import 的 Babel 配置
如果代码使用了动态导入(import('some-file.js').then(module => ...)),需要启用dynamic-import-node插件,并配合syntax-dynamic-import语法插件。推荐按环境区分配置,只在test环境启用该插件:
{ "presets": [["env", {"modules": false}]], "plugins": ["syntax-dynamic-import"], "env": { "test": { "plugins": ["dynamic-import-node"] } } }这样在开发/构建环境保留原生动态 import 行为,而在 Jest 的 Node 测试环境中将其转换为 CommonJS 形式,避免 Node 无法直接解析 ESM 动态导入的问题。
小结:webpack 到 Jest 的配置映射速查表
| webpack 配置 | Jest 配置 | 说明 |
|---|---|---|
module.rules中的 JS 编译 | transform+babel-jest | 默认自动启用;多预处理器时需显式声明 |
module.rules中的资源加载 | moduleNameMapper→ Mock 文件 | 样式返回{},文件返回'test-file-stub' |
| CSS Modules | moduleNameMapper→identity-obj-proxy | className 原样返回 |
| 自定义资源处理 | transform→ 自定义 transformer | 例如返回文件 basename |
resolve.extensions | moduleFileExtensions | 可省略扩展名 |
resolve.modules | moduleDirectories | 递归向上查找的目录名 |
resolve.roots/NODE_PATH | modulePaths | 额外的绝对查找路径 |
resolve.alias | moduleNameMapper正则映射 | 用^react(.*)$/$1等复刻别名 |
webpack 是一个复杂而灵活的工具,针对具体应用的特定需求,你可能需要进一步微调配置;但对大多数项目而言,Jest 的配置体系足以完整承接 webpack 的模块解析与资源处理能力。对于更复杂的 webpack 配置,可以进一步研究babel-plugin-webpack-loaders之类的生态工具,也可以参考 Jest 30 官方文档中的 Configuration.md 获取每个配置项的完整说明。想在一个 React + webpack 项目中亲自上手,可参照本文从一份真实 webpack 配置出发,逐步翻译出对应的 Jest 配置并运行验证。
【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考