Jest 已经成为了大部分前端项目的标配,每次说到 JestWebpackESLint 等配置,脑瓜子都嗡嗡的。在诸多配置中,有时一个“铆钉大”的配置,就能让程序或测试的运行效率大幅下降。至于为啥要写这篇文章,就是因为目前所在的项目因一处 Jest 配置的问题,导致60多个 test case--no-cache 条件下要跑足足 790s

所以就记录分享一下 Jest 的一些常用配置。

module.exports = {
  setupFiles: [
    'react-app-polyfill/jsdom',
    '<rootDir>/test/unit/jest.setup.js',
    'core-js',
  ],
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{spec,test}.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
  ],
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
    '^.+\\.(css|less)$': '<rootDir>/config/jest/cssTransform.js',
    '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '<rootDir>/config/jest/fileTransform.js',
  },
  transformIgnorePatterns: [
    '[/\\\\]node_modules/(?!(antd)/)[/\\\\].+\\.(js|jsx|ts|tsx)$',
  ],
  moduleNameMapper: {
    '^react-native$': 'react-native-web',
    '^.+\\.module\\.(css|sass|scss|less)$': 'identity-obj-proxy',
    '\\.svg$': 'identity-obj-proxy',
    'test/(.*)': '<rootDir>/test/$1',
    '^src/(.*)': '<rootDir>/src/$1',
  },
  moduleFileExtensions: [
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    'node',
  ],
  // 其它配置已省略
};

对于 Jest 的配置优化无外乎下面两点:

  • 更少:减少不必要的元素(比如图片、样式等);
  • 更精确:减少在文件系统中查找匹配的时间;

在看下面如何优化之前,可以先看下这份 Jest 配置,看一下有没有什么可以想到的优化点

setupFiles

古语云:“思则有备,有备无患”。但跑测试时一定要“万事”俱备才行吗?

setupFiles 可谓是 Jest 的“内务府大臣”,位居二品!此大臣就是来准备测试所需要的一些环境或 mock 一些全局状态的,比如 @testing-library/jest-dom 就常在 setupFile 中用到,它允许我们可以在 Jest 中断言一些关于 DOM 的状态。而我们再回头去看上面的配置:

{
  "setupFiles": [
    "react-app-polyfill/jsdom",
    "<rootDir>/test/unit/jest.setup.js",
    "core-js"
  ]
}

react-app-polyfill/jsdom 做了什么:

// 真的,源码只有这三行,原来“内务府”也有“外包项目” 
if (typeof window !== 'undefined') {
  require ('whatwg-fetch');
}

whatwg-fetchcore-js 可以简单理解为对当下的一些新标准做 polyfill,但我们有 babel-jest 呀,还要你二者何用?果断“开掉”,节省开支

{
  "setupFiles": ["<rootDir>/test/unit/jest.setup.js"]
}

moduleFileExtensions

moduleFileExtensions 就是 Jest 中各个“国家”(模块)的“通关文牒”,有此文牒方可游历各国。游遍各国也得有个顺序不是,不然会徒增“食宿饮马等费用”,要是搁徐霞客身上,若不是他家业富足,不然大可能会饿死途中。张骞通西域朝廷会有路线规划(默认配置),当然他也能随机应变(自定义配置),默认配置大部分情况下是行得通的,只不过可能要在“路上”多花些时间:

// 默认配置
["js", "jsx", "ts", "tsx", "json", "node"]

moduleFileExtensions从左到右 查找对应的 extension,但如果在 TypeScript + React 项目中可能稍微调整一下会更好:

// 调整后
["ts", "tsx", "js", "jsx", "json", "node"]

这样就能减少些查找 extension 的次数,省点“油钱”。

moduleNameMapper

一般 npm 依赖中的源码分为 esmcjs 模块,当然像 react 之类的是分为 cjsumd。以 antd 为例,其结构如下:

antd
  |- lib/
  |- es/
  |- package.json

其中,在 package.json 中可以制定 esmcjs 打包文件的目录,以 antd 中使用到的 rc-select 组件包为例:

{
  "version": "12.1.5",
  "main": "./lib/index",
  "module": "./es/index"
}

如要使用 cjs 规范的打包文件,工具会查询 main 字段对应文件路径内的入口文件,使用 esm 规范的打包文件,工具则会查询 module 字段对应的入口文件。

但在 rc-util 中,并没有指明 mainmodule 字段,那么其使用方式就像下面这样:

// rc-select/es/utils/legacyUtils.js
import toArray from 'rc-util/es/Children/toArray';

“聊 Jest 呢,叨叨上面这么多干嘛呢?”

因为 Jest 目前支持的是 cjs 规范,项目中又用到了 antd,所以对于其使用的 rc-util 这种依赖,Jest 无法处理,需要手动转换一下,这就需要引入一个 Jest 配置字段 —— moduleNameMapper,关于该配置字段的描述文档如下所述:

A map from regular expressions to module names that allow to stub out resources, like images or styles with a single module.

说白了就是用来 stub 一些资源文件或 module 使用的,可以把匹配到的内容映射为你指定的内容,哪怕是“指鹿为马”也是行得通的!在前端的单元测试中,时常有许多内容是不需要的,比如:静态资源、样式文件等。那么这个时候就可以将这些“鹿”指成“马”了。

我们常把“鹿 ”指为 identity-obj-proxy 这个工具,虽然 identity-obj-proxy 上次发布是5年前了,但确实很好用,并且源码也十分简单(2分钟你看不完源码你顺着信号 来打我)!

module.exports = {
  // ...
  moduleNameMapper: {
    '\\.svg$': 'identity-obj-proxy',
    '\\.css$': 'identity-obj-proxy',
  },
};

对于上面说到的将 antdes 指到 lib 也就很简单了:

module.exports = {
  // ...
  moduleNameMapper: {
    'antd/es/(.*)': 'antd/lib/$1',
  },
};

通过 moduleNameMapper 就可以做到 更少 这个原则,当然下面要介绍的 transformIgnorePatterns 以及其它 ignore 等相关字段也可以让处理的资源或无关的资源更少。

transform

《天龙八部》中,马大元夫人康敏将一招“借刀杀人”发挥得淋漓尽致,而在 Jesttransform 也“借他人之手除掉异己”,至于康敏居心何在下回再讲,这次就说 transform 为何要“下此毒手”。所谓“异己”一般是脱离自己控制的资源,上面说到 Jest 支持的是 cjs,但在现在的前端项目中,一般都是使用 import/exportesm 规范来模块化开发,所以对于这种资源,我们需“借他之手”处理:

module.exports = {
    // ...
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};

但这么看来,transform 也可以将 antd 中的 esm 资源转为 cjs,但既然可以“礼仪教化”,又何必“兵刃相接”呢?

transformIgnorePatterns

通过名字就能看出来,此配置的内容是“康敏”理都不想理的内容,该值默认是 ['node_modules'],也十分好理解。但我们回到文章最初的配置去看看:

module.exports = {
  // ...
  transformIgnorePatterns: [
    '[/\\\\]node_modules/(?!(antd)/)[/\\\\].+\\.(js|jsx|ts|tsx)$',
  ],
};

项目的初衷是使用 transform 去处理引入的 antd 的资源,但这也就导致了在 transform 时去遍历了整个 node_modules 文件系统,node_modules 内容是非常多的,所以在扫描时耗费了大量的时间,测试跑完发现“乔峰找着他爹了”。

所以本文最开始所说的“一处配置问题”就是这儿,删掉 transformIgnorePatterns 转而使用 moduleNameMapper 会快很多,不信你试试!

结语

当优化、最佳实践等问题脱离具体项目场景时,那就是“耍流氓”!上学时老师经常说:“具体问题,具体分析”,对开发来说也是如此。经过优化后,测试消耗时间从 790s 缩减到了 40s,还算是一个可以接受的时间吧✌️

欢迎搜索关注微信公众号 “Refactor”,阅读我的更多文章。