開發紀錄

使用 ts-jest 測試相依性中包含 ESM 的專案

Kevin Cheng
Cow Say
Published in
6 min readOct 12, 2021

--

我有一個 TypeScript 專案 (日後會再發一篇文來介紹關於這個專案),我打算使用 ts-jest 撰寫單元測試,然而在我的相依性 (Dependency) 中包含了數個 ESM 模組,造成我以 jest 執行測試時報了以下錯誤:

FAIL  test/index.test.ts
● Test suite failed to run
Jest encountered an unexpected tokenJest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.By default "node_modules" folder is ignored by transformers.Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/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.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:<PROJECT>\node_modules\pupa\index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { htmlEscape } from 'escape-goat';
^^^^^^
SyntaxError: Cannot use import statement outside a module

不得不說 Jest 的報錯真的非常詳細!但現在還不是佩服的時候,待問題解決再來回頭欣賞一番。

在本次問題中,我決定採用「將所有 ESM 編譯為 CJS」的方式解決。

Jest Transform

從報錯中可以看到有兩個 Jest 配置至關重要,分別是 transformtransformIgnorePatterns

transform

transform 預期是一個物件,key 為正規表達式的字串 (會以 new RegExp(key) 於執行時轉為 RegExp 物件),value 為 transformer 名稱和配置 (只有指定名稱時可以只給一個字串如 "babel-jest",若包含配置則給一個陣列,陣列第一項為名稱字串、第二項為配置物件,如 ["babel-jest", {...configs}])。

預設配置為

{"\\.[jt]sx?$": "babel-jest"}

transformIgnorePatterns

transformIgnorePatterns 為一個陣列,用於匹配所有程式檔案名稱,匹配到的檔案就不會進行轉譯。

在 Jest 的預設行為中就會自動使用 babel-jest 為專案原始碼進行轉譯,但這個行為忽略所有 node_modules/ 下面的項目。

預設配置為

["/node_modules/", "\\.pnp\\.[^\\\/]+$"]

ts-jest

在 ts-jest 的官網中是這麼提到自己:

ts-jest is a Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.

在了解 Jest 的預設行為之後,不可忽視的是,ts-jest 本質上也是一個 transformer,和 babel-jest 處於對等關係。

ts-jest 在 Jest 的配置中通常以 preset 的方式登場,也就是 ts-jest 會代替我們修改部分設定。

對於 transform ,ts-jest 將它改為

{ "^.+\\\\.tsx?$": "ts-jest" }

(參考來源)

由於原先 Jest 預設的 transform 配置已經被覆蓋了,因此 babel-jest 並不會被呼叫。

對於當前遇到這個 ESM 編譯的問題,我們應該要將 ESM 模組都使用 babel-jest 編譯為 CJS。

解決方案

對於 transform 我將所有檔名結尾為 .mjs、.js、.jsx 都引導至 babel-jest。

接著我發現 babel-jest 並不會直接將 ESM 轉譯為 CJS,所以增加了一個插件 @babel/plugin-transform-modules-commonjs

然後是 transformIgnorePatterns,我將所有 ESM 以白名單的方式寫入正則匹配裡面,不然它們會因為是 node_modules 而被 Jest 預設的配置忽略。

整體配置如下:

// jest.config.jsconst path = require('path')module.exports = {
preset: 'ts-jest',
globals: {
"ts-jest": {
tsconfig: path.join(__dirname, 'tsconfig-test.json')
}
},
transform: {
'\\.m?jsx?$': [
'babel-jest',
{
plugins: [
'@babel/plugin-transform-modules-commonjs'
]
}
],
'\\.tsx?$': 'ts-jest'
}
transformIgnorePatterns: [
// for cross platform
`node_modules\\${path.sep}(?!(pupa|escape-goat))`
]
}

在此簡單紀錄了我在使用 ts-jest 遇到的問題和解決方法,也藉此更進一步摸索了 Jest Transform 這一部分的功能。

--

--

Kevin Cheng
Cow Say

貓奴 / 後端工程師 / 人生最重要的四件事:溫柔、勇敢、自由、浪漫