用 vitest 吧!单测执行速度更快问题更少!- 2023-09-27 更新
本文是解决 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's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• 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 替换成 cjs
'pkg1/esm/(.*)': '<rootDir>/node_modules/pkg1/cjs/$1',
// 比如常见的 lodash-es
'lodash-es/(.*)': 'lodash/$1',
},
优点
- 无编译,单测执行快 🚀;
- 新增 api 无需增加大量的 mock。而且也不现实,因为有些 api 很复杂没法 mock,相当于重写三方依赖了。
缺点
对应的三方库必须有 cjs 版本。
解决方案二:mock 三方模块 ✅
在闲暇时间花了点时间用手机粗略阅读了下 jest 文档,发现模块都可以被 mock,瞬间天朗气清。
以 TS 项目为例:
1 jest.config.js
设置两个字段即可。
setupFiles
内含 mock 逻辑,需要在单测运行前执行,故放到 jest.setup.js 中。- 运行环境从 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-array
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 内容如下:
// 这些是报错的模块,通过 mock 搞定
// https://dev.to/dstrekelj/how-to-mock-imported-functions-with-jest-3pfl
jest.mock('some-esm', () => ({}));
jest.mock('other-esm', () => ({
AClass: class AClass {
foo() {}
},
}));
jest.mock('other-esm-2', () => ({
isIOS: true,
}));
// 以下是不存在的全局对象,通过设置到 global 搞定
const Bar = {
baz({ success }) {
success(Date.now());
},
};
// mock window.Bar in testEnvironment=jsdom
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
orNODE_OPTIONS=--experimental-vm-modules npx jest
; -
package.json 增加 type = module。
一般我们都会选择方式 1,本文在其基础上让其支持测试三方 esm 模块。
参考
-
让 jest 支持测试业务 ESM 代码:jestjs.io/docs/ecmasc…
TODO
- npm 包 create-test-app 增加 jest.setup.js