本文是解决 node_modules 导致单测失败,如果是业务代码导致报错请参考
让 Jest 支持测试 ESM 业务代码
。
因为业界的解决方案都不适用且有更好的解决方案,故记录之。
若能帮助到你不妨点赞支持 😄。
一般来说通过
create-test-app
cli 一键配置单测环境即可正常运行单测,但是一旦单测引入的三方模块是 esm 就会出现『Cannot use import statement outside a module』或『Cannot use export statement outside a module』。日志如下:
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
Details:
src/node_modules/some-esm/esm/foo.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import{__awaiter as t,__generator as r}from"tslib";import{isPlainObject as n}from};
SyntaxError: Cannot use import statement outside a module
从描述得知,单测引入的文件是 ECMAScript Modules 即 esm 格式,jest 没法处理,刚好报错文件确实是一个 esm。
基于以下两个事实:
jest 官方文档配置的 babel.config.js 默认只会编译业务代码,
jest 不会对 node modules 内的文件编译,故配置 babel 并不能直接解决该问题。
从错误描述很自然可以得到一种解决方案,即方案三:开发者显示告诉 babel 必须编译哪些出错的三方包即可。
下面我们将从优到劣列出所有解决方案。
解决方案一:映射到 cjs ✅
一般三方模块都会同时编译一份 esm 和 cjs,故我们在单测中将其映射成 cjs 即可正常运行单测。
jest.config.js
moduleNameMapper: {
'pkg1/esm/(.*)': '<rootDir>/node_modules/pkg1/cjs/$1',
'lodash-es/(.*)': 'lodash/$1',
无编译,单测执行快 🚀;
新增 api 无需增加大量的 mock,如果有些 api 很复杂也没法 mock,否则相当于重写三方依赖了。
对应的三方库必须有 cjs 版本。
解决方案二:mock 三方模块 ✅
在闲暇时间花了点时间用手机粗略阅读了下 jest 文档,发现模块都可以被 mock,瞬间天清地朗。
以 TS 项目为例:
1 jest.config.js
设置两个字段即可。1 setupFiles 内含 mock 逻辑,需要在单测运行前执行,故放到 jest.setup.js 中。2 运行环境从 node 改成 jsdom,因为我们是 H5 项目依赖了 window 和 self,改成 jsdom 能减少诸多不必要的 mock。当然第二步并非必要,而且从单测速度来看,两环境相差不大。
+ setupFiles: ['./jest.setup.js'],
- testEnvironment: 'node',
+ testEnvironment: 'jsdom',
完整内容:
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
transform: {
// ts 项目无需
'^.+\.(js)$': 'babel-jest',
// 🔥 https://jestjs.io/zh-Hans/docs/configuration
setupFiles: ['./jest.setup.js'],
preset: 'ts-jest',
// 🔥 fix > ReferenceError: self is not defined
testEnvironment: 'jsdom',
// 以下字段非关键字段,可忽略
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
coverageProvider: 'v8',
coverageDirectory: 'coverage',
2 jest.setup.js
将报错的三方模块 mock 即可,同时按需 mock 一些不存在的对象。故 mock 内容如下:
jest.mock('some-esm', () => ({}));
jest.mock('other-esm', () => ({
AClass: class AClass {
foo() {}
jest.mock('other-esm-2', () => ({
isIOS: true,
const Bar = {
baz({ success }) {
success(Date.now());
global.Bar = Bar;
mock 方案更简单可控,无需编译三方模块,速度更快 🚀。
需按需 mock 模块。三方模块新增的 api 若单测使用了,需手动添加 mock。
有些 api 还没法 mock,否则相当于实现了三方库。
解决方案三:编译三方模块
jest.config.js transformIgnorePatterns
+ transformIgnorePatterns: ['node_modules/(?!(some-esm)/)'],
执行一次 test,然后发现还有其他包报错,则继续加入即可。语法是用 |分隔:
+ transformIgnorePatterns: ['node_modules/(?!(@some-esm|other-esm)/)'],
解决方案符合直觉 😄。
单测执行速度慢,三方模块编译会拖慢整个单测时间;
non-determinstic。编译方式可能和最终代码发布版本的不一致,导致测试结果不具备信服力;
配置和正则都是逆向逻辑,难以理解。
吐槽一句,jest 配置其实比 mocha 还要复杂,而且执行慢一个文件需要 12s-18s,不够 joyful 😄。
其次 transformIgnorePatterns这个命名让配置理解起来很绕 🙄,本身是逆向逻辑,然后其内的正则表达式还要取反,双重否定,最终表达的配置意图是 A 和 B 模块需要编译,而 transformIgnorePatterns 的字面意思是无需编译的模块 😡。
jest 支持测试业务 ESM 代码,注意这是本文的前提。如何让其支持官方列出了三种方式:
transform + babel.config.js(creat-test-app 已支持);
打开 node 实验性质的 esm flag:node --experimental-vm-modules node_modules/jest/bin/jest.js or NODE_OPTIONS=--experimental-vm-modules npx jest;
package.json 增加 type = module。
一般我们都会选择方式 1,本文在其基础上让其支持测试三方 esm 模块。
dev.to/dstrekelj/h…
让 jest 支持测试业务 ESM 代码:jestjs.io/docs/ecmasc…