news 2026/9/19 15:10:06

Jest 代码转换(Code Transformation)完全指南:从 babel-jest 默认配置到自定义 Transformer 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Jest 代码转换(Code Transformation)完全指南:从 babel-jest 默认配置到自定义 Transformer 实战

Jest 代码转换(Code Transformation)完全指南:从 babel-jest 默认配置到自定义 Transformer 实战

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

Jest 默认以纯 JavaScript 执行项目代码,当你的源码或测试使用了 Node 原生不支持的语法(如 JSX、TypeScript、Vue 模板)时,就需要通过代码转换(Code Transformation)将其转译为普通 JavaScript。本文以 docs/CodeTransformation.md 为骨架,结合 packages/babel-jest、packages/jest-transform、packages/babel-preset-jest 等仓库源码,系统讲解transform配置、babel-jest默认行为、Transformer 完整 API、同步/异步转换机制与缓存失效原理,并给出可复制的自定义 Transformer 实战代码,帮助你为任意文件类型(图片、CSS、模板引擎等)编写转换器并接入 Jest。

为什么需要代码转换

Jest 本身并不理解 JSX、TypeScript 类型注解、Vue 模板等语法——它的运行时环境是 Node 的 JavaScript 引擎。因此,与"为浏览器打包"类似,你需要在测试运行前把这些语法转换成 Node 可以直接执行的纯 JavaScript。Jest 通过transform配置项支持这一机制。

一个 transformer(转换器)就是"提供了转换源码文件方法的模块"。例如,当你想在模块或测试中使用 Node 尚未支持的新语言特性时,可以接入一个代码预处理器,把未来版本的 JavaScript 转译成当前版本。

值得强调的是,Jest 会缓存转换结果,并根据多种因素尝试使缓存失效,例如被转换文件的源码内容、Jest 配置的变化等。理解缓存机制(见下文"缓存与缓存键"一节)对于编写高性能转换器至关重要。

默认行为:开箱即用的 babel-jest

Jest 自带一个开箱即用的转换器 ——babel-jest(源码位于 packages/babel-jest/src/index.ts)。它的行为如下:

  • 加载项目自身的 Babel 配置babel-jest会读取你项目根目录下的 Babel 配置文件(如babel.config.js.babelrc),并使用这些配置转译代码;
  • 匹配文件范围:转换所有匹配/\.[jt]sx?$/正则的文件,即.js.jsx.ts.tsx四种扩展名;
  • 注入 mock 提升(hoisting)插件babel-jest会自动注入使jest.mock提升生效的 Babel 插件,具体机制参见 ES Module mocking。

transform配置项的默认值是{"\\.[jt]sx?$": "babel-jest"}(见 docs/Configuration.md)。这意味着只要安装了jest-cli,并执行yarn add --dev babel-jest @babel/core,JS/TS 代码就会自动经过 Babel 转换(参见 packages/babel-jest/README.md)。

关于 babel-preset-jest

默认情况下,babel-jest会额外注入babel-preset-jest。从 packages/babel-preset-jest/index.js 可以看到,这个 preset 由两部分组成:

const jestPreset = { plugins: [require.resolve('babel-plugin-jest-hoist')], presets: [require.resolve('babel-preset-current-node-syntax')], };

babel-plugin-jest-hoist(负责jest.mock等调用的提升)和babel-preset-current-node-syntax(让 Babel 输出的代码语法与当前 Node 版本能力对齐)。

在 packages/babel-jest/src/index.ts 的createTransformer中可以看到注入逻辑:

const {excludeJestPreset, ...inputOptions} = transformerConfig ?? {}; // ... presets: [ ...(inputOptions.presets ?? []), ...(excludeJestPreset === true ? [] : [jestPresetPath]), ],

也就是说,excludeJestPreset: true会阻止注入该 preset。但请注意:禁用它将同时停止jest.mock的提升(hoisting),可能导致你的测试失效。配置方式如下:

"transform": { "\\.[jt]sx?$": ["babel-jest", { "excludeJestPreset": true }] }

与其他预处理器并存:必须显式声明 babel-jest

如果你要同时使用多个代码预处理器,务必显式把默认的babel-jest也写在transform中,否则.js/.ts文件将不再被转换:

"transform": { "\\.[jt]sx?$": "babel-jest", "\\.css$": "some-css-transformer" }

babel-jest还支持传入任意 Babel 选项,例如{"\\.js$": ["babel-jest", {rootMode: "upward"}]},或者像官方 README 示例那样指定extends与额外插件:

"transform": { "\\.[jt]sx?$": ["babel-jest", { "extends": "./babel.config.js", "plugins": ["babel-plugin-transform-import-meta"] }] }

编写自定义 Transformer:完整 API

你可以编写自己的转换器。Jest 的 Transformer 接口定义在 packages/jest-transform/src/types.ts,其核心类型如下(为便于阅读做了精简,完整定义请查阅仓库源码):

interface TransformOptions<TransformerConfig = unknown> { supportsDynamicImport: boolean; supportsExportNamespaceFrom: boolean; /** * 取值为: * - 若 Jest 未以 Node ESM 标志 `--experimental-vm-modules` 运行,则为 `false` * - 若文件扩展名定义在 [extensionsToTreatAsEsm](https://link.gitcode.com/i/96937fa2ceda18dba0a1ae29b9959f19) 中, * 且 Jest 以 `--experimental-vm-modules` 运行,则为 `true` */ supportsStaticESM: boolean; supportsTopLevelAwait: boolean; instrument: boolean; /** 由 `jest-runtime` 使用的缓存文件系统,用于提升性能。 */ cacheFS: Map<string, string>; /** 当前运行项目的 Jest 配置。 */ config: ProjectConfig; /** `config` 的字符串化版本——常用于缓存失效(cache busting)。 */ configString: string; /** 用户通过 `transform` 选项传入的转换器配置。 */ transformerConfig: TransformerConfig; } type TransformedSource = { code: string; map?: RawSourceMap | string | null; }; interface SyncTransformer<TransformerConfig = unknown> { canInstrument?: boolean; getCacheKey?: (sourceText, sourcePath, options) => string; getCacheKeyAsync?: (sourceText, sourcePath, options) => Promise<string>; process: (sourceText, sourcePath, options) => TransformedSource; processAsync?: (sourceText, sourcePath, options) => Promise<TransformedSource>; } interface AsyncTransformer<TransformerConfig = unknown> { canInstrument?: boolean; getCacheKey?: (sourceText, sourcePath, options) => string; getCacheKeyAsync?: (sourceText, sourcePath, options) => Promise<string>; process?: (sourceText, sourcePath, options) => TransformedSource; processAsync: (sourceText, sourcePath, options) => Promise<TransformedSource>; } type Transformer<TransformerConfig = unknown> = SyncTransformer<TransformerConfig> | AsyncTransformer<TransformerConfig>; type TransformerCreator<X extends Transformer, TransformerConfig = unknown> = (transformerConfig?: TransformerConfig) => X; type TransformerFactory<X extends Transformer> = { createTransformer: TransformerCreator<X>; };

从源码结构看,TransformOptions中的supports*系列标志来自CallerTransformOptions(在 packages/jest-transform/src/types.ts 中与Options一起被引用),它们用于告知转换器"应当输出 ESM 还是 CJS",与同步/异步本身没有直接关系。

同步与异步转换:理解 process / processAsync

Jest 按需(on demand)对文件进行代码转换:当requireimport被求值时才触发。这一过程(也叫 transpilation)可能是:

  • 同步的:发生在require时;
  • 异步的:发生在importimport()时(后者也能在 CommonJS 模块中使用)。

因此接口提供了成对的方法:process{Async}getCacheKey{Async}。后者用于判断"是否真的需要调用process{Async}"——即先计算缓存键,命中磁盘缓存则直接复用,避免重复转译。

两个重要的兼容性规则:

  1. 异步可以回退到同步:如果processAsync未实现,异步转译会回退到同步的process
  2. 同步不能调用异步:同步转译永远无法使用processAsync

因此,如果你的代码库只使用 ESM,实现异步变体即可;但只要有任何代码通过require加载(包括 ESM 内部通过createRequire加载),就必须实现同步的process变体。Transformer类型定义中的注释也印证了这一点(见 packages/jest-transform/src/types.ts)。

与转换方式密切相关的supports*标志含义如下(参见 ECMAScriptModules 文档):

  • supportsDynamicImport: true:转换器可以返回import()表达式,ESM 与 CJS 都支持;
  • supportsStaticESM: true:顶层import语句被支持,返回的代码将按 ESM(而非 CJS)解释执行。

缓存与缓存键:为什么要实现 getCacheKey

虽然不强制,但官方强烈建议实现getCacheKey,否则每次都要重新转译,而不是从磁盘读取上一次的结果,白白浪费资源。

在 packages/jest-transform/src/ScriptTransformer.ts 中,ScriptTransformer负责协调整个转换流程:先按文件匹配transform正则,加载对应的 transformer(支持直接导出对象,或导出带createTransformer的工厂,见loadTransformers),然后计算缓存键、调用process、把结果写入磁盘缓存目录(jest-transform-cache-<id>,按缓存键前两位分子目录存放)。

缓存键的默认计算(_buildCacheKeyFromFileInfo)会综合以下要素做 SHA-1 摘要:

  • 文件内容(fileData);
  • 序列化后的 Jest 配置(configString);
  • 是否开启插桩(instrument);
  • callerSupport信息(supports*标志);
  • 文件路径与内部CACHE_VERSION

babel-jestgetCacheKey实现则进一步加入了 Babel 配置本身(见 packages/babel-jest/src/index.ts):它会 SHA-1 摘要babel-jest自身的代码、序列化后的 Babel options、源码文本、相对路径、Jest 配置字符串、Babel 配置文件路径、是否插桩、NODE_ENVBABEL_ENV以及 Node 版本号。这意味着修改 Babel 配置或环境变量都会导致缓存失效,行为完全可预期。

官方提供了一个辅助包@jest/create-cache-key-function帮助实现getCacheKey(仓库源码见 packages/jest-create-cache-key-function)。

开发期间的调试建议

在开发转换器的过程中,可以配合--no-cache运行 Jest 来绕过缓存,并在需要时手动清空缓存目录(参见 缓存问题排查)。

工厂模式:createTransformer 与转换器配置

除了直接实现Transformer接口,你还可以选择导出createTransformer—— 一个用于动态创建转换器的工厂函数。这样做的好处是可以在 Jest 配置中传入转换器配置:当transform配置的值为['path-to-transformer', {options}]这种元组形式时,ScriptTransformer.loadTransformers会检测到工厂并调用createTransformer(transformerConfig)(见 packages/jest-transform/src/ScriptTransformer.ts)。

babel-jest本身就是这一模式的范例:它在 packages/babel-jest/src/index.ts 导出createTransformer,接受{excludeJestPreset, ...}等配置;同时为了兼容 Jest 的requireOrImportModule加载方式,把工厂挂到了default导出上:

const transformerFactory = { createTransformer, }; export default transformerFactory;

关于 transformIgnorePatterns:node_modules 默认不转换

请注意,默认配置下node_modules不会被转译,如需转译必须修改transformIgnorePatterns配置项(默认值为["/node_modules/", "\\.pnp\\.[^\\/\\\\]+$"],见 docs/Configuration.md)。

该配置项是一个正则字符串数组,在转换前与所有源文件路径匹配:只要匹配其中任意一个模式,该文件就不会被转换。常见的实战场景是:某些第三方模块(尤其是 React Native、TypeScript 项目)以未转译的 ES 代码发布,此时需要显式放行:

// jest.config.js module.exports = { transformIgnorePatterns: [ '/node_modules/(?!(foo|bar)/)', ], };

上例会转译node_modules/foo/node_modules/bar/下的文件。注意,相互重叠的多个模式可能导致"你以为会转换、实际却没转换"的文件——例如再加一个'/bar/'模式,node_modules/bar/会因匹配第二个模式而仍然不被转换。模式字符串匹配的是完整路径,建议使用<rootDir>令牌锚定项目根目录,避免在不同环境中误伤所有文件,例如'<rootDir>/bower_components/''<rootDir>/node_modules/'

另外,如果使用pnpmnode_modules下的包是通过符号链接指向.pnpm目录的,直接写<rootDir>/node_modules/(?!(package-a)/)不会生效,需要改用<rootDir>/node_modules/.pnpm/(?!(package-a|@scope\\+pkg-b)@)之类的模式。

源码映射与覆盖率:process 返回值的关键细节

官方特别提醒:务必让process{Async}返回 source map(与转译后的代码一起),这样代码覆盖率和测试错误报告才能准确定位到源码行号。内联 source map 也可以工作,但性能更慢。

babel-jest的实现看,它在构造 Babel 选项时固定设置了sourceMaps: 'both'(同时产出内联与外置 map,见 packages/babel-jest/src/index.ts),并在process中把{code, map}一并返回。当启用覆盖率插桩时(transformOptions.instrument为真),它还会注入babel-plugin-istanbul插件完成插桩(addIstanbulInstrumentation)。

如果你的转换器声明了canInstrument: true,Jest 会认为返回的代码已自行插桩;若为false或未声明,Jest 会在转换后用 Babel 对返回代码执行 Istanbul 插桩(参见 packages/jest-transform/src/types.ts 与 packages/jest-transform/src/ScriptTransformer.ts 中的_instrumentFile)。

实战示例一:TypeScript 类型检查(ts-jest)

babel-jest虽然能转译 TypeScript 文件,但 Babel 的转译是"语法剥离",不会做类型检查。如果你希望在测试时同步进行类型校验,可以使用ts-jest作为转换器(配置方式为将transform中 TS 文件的正则指向ts-jest)。它把转译与类型检查结合在一条流水线里。

实战示例二:把图片转换为路径(fileTransformer)

导入图片是浏览器打包中的常见做法,但图片本身不是合法 JavaScript。在 Jest 中一种优雅的处理方式是:写一个转换器,把导入的值替换为图片文件名。官方文档给出如下实现:

const path = require('path'); module.exports = { process(sourceText, sourcePath, options) { return { code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`, }; }, };
module.exports = { transform: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/fileTransformer.js', }, };

这个例子展示了转换器最核心的形态:接收sourceTextsourcePathoptions三个参数,返回{code}(必要时附带map)。module.exports = "xxx.jpg"这样的输出让require('./logo.png')在测试中直接得到文件名。注意:由于默认transformIgnorePatterns排除node_modules,如果你后续想把这个转换器用于node_modules内的资源,需要相应调整该配置。

实战示例三:模板引擎预编译(仓库内真实用例)

仓库的端到端测试 e2e/coverage-handlebars/transform-handlebars.js 提供了一个真实的自定义转换器:它把 Handlebars 模板预编译为 JavaScript,并返回带 source map 的代码,从而让覆盖率能正确映射回.hbs源文件:

const {build} = require('@jridgewell/build-mapping'); const Handlebars = require('handlebars/dist/cjs/handlebars.js'); const dedent = require('string-dedent'); exports.process = (code, filename) => { const pc = Handlebars.precompile(code, {srcName: filename}); return dedent(build)` const Handlebars = require("handlebars/dist/cjs/handlebars.runtime.js"); module.exports = Handlebars.template(${pc}); `; };

配合该目录的 jest 配置,"\\.hbs$"会命中这个转换器。这说明:只要返回合法的 JavaScript 与可选的 source map,任何文件格式都能接入 Jest。相关的端到端测试(如 e2e/tests/coverageHandlebars.test.ts)验证了转换后的覆盖率映射行为。

总结

代码转换是 Jest 支持非 JavaScript 语法与自定义资源类型的核心机制,脉络清晰:

  1. 默认即用babel-jest开箱即用地转换.js/.jsx/.ts/.tsx,加载项目 Babel 配置并注入 mock 提升所需的 preset;
  2. 可扩展:通过transform配置可以叠加任意转换器,但别忘了显式保留babel-jest
  3. 接口明确:实现process/processAsync(按需选配getCacheKey系列),同步/异步的回退规则决定了你的代码库必须支持哪种形态;
  4. 性能关键:实现getCacheKey才能充分利用磁盘缓存,避免重复转译;
  5. 边界清晰transformIgnorePatterns默认排除node_modules,需要转译第三方未编译代码时按需放行;
  6. 质量保障:返回 source map 是覆盖率与错误定位准确性的前提,canInstrument则决定了是否由 Jest 代为插桩。

从图片路径替换到 Handlebars 预编译,再到仓库端到端测试中的真实用例,掌握了 Transformer 接口,你就能让 Jest 理解项目中任何一种文件格式。

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 15:07:04

无管理员权限Mac上NVM安装与Node多版本管理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 15:06:13

electerm 插件市场怎么用:5分钟完成第一次配置的完整指南

electerm 插件市场怎么用&#xff1a;5分钟完成第一次配置的完整指南 【免费下载链接】electerm &#x1f4fb;Free and open-sourced terminal/ssh/sftp/ftp/telnet/serialport/RDP/VNC/Spice client(Linux, Mac, Windows, Android, HarmonyOS, iOS) 项目地址: https://gitc…

作者头像 李华
网站建设 2026/9/19 14:58:38

人工智能驱动的税务审计异常检测:从规则引擎到机器学习

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 14:55:28

IBM DS存储阵列管理:Storage Manager安装与SMI-S连接排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华