添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

本文是解决 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

    设置两个字段即可。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-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 or NODE_OPTIONS=--experimental-vm-modules npx jest

    package.json 增加 type = module。

    一般我们都会选择方式 1,本文在其基础上让其支持测试三方 esm 模块。

    dev.to/dstrekelj/h…

    让 jest 支持测试业务 ESM 代码:jestjs.io/docs/ecmasc…