Jest: Not So Delightful Anymore
Several months ago, I had the hardest time setting up Jest with a React 18 Typescript project as part of a spike to help teams who wanted to use Jest to test web components that were built with ESM dependencies.
As I polled my peers working on React products, many mentioned they had moved over to Vitest.
The outlook for Jest leading up to this moment hasn't been great. In 2022, one of Jest's OSS maintainers mentioned "no one at Facebook... worked on Jest for years". The project was subsequently transferred to the OpenJS foundation for maintenance and development has slowly fizzled since.
As of December 2023, Jest support for esmodules is still experimental due to its unfortunate reliance on node's vm module for test isolation.
If you discover yourself at a large company where tech isn't a core competency, you might realize there's a world where many revenue-generating products are still chugging along, one-Jenga-move-from-collapse, on ancient versions of Angular and React. A significant amount of time working as a dev at such a company will be spent helping teams figure out how to integrate extremely outdated frameworks and tools with modern web things.
If I'm appropriately tactful, I might eventually persuade them to migrate and upgrade their already end-of-life-d framework if they realize the diminishing returns on clinging to legacy technical choices that only compounds in friction to new feature delivery...
More often, organizational politics make it such that you have to just make something irrelevantly old work with something rather new (and possibly immature) because upgrading or changing tools isn't an imaginable option for said team.
Investigating how last 2 versions of Create React App (deprecated) and Jest could seamlessly import and test ESM web component library was the laughable situation I found myself in.
Oh yeah here's the sample StackBlitz if you don't feel like reading.
Overcoming Configuration Hell
Rotating through some hell combinations of ts-jest
, ts-node
, and babel
plugins as recommended by different official documentation sources, I rehashed the following to get it working:
- Use the
node --experimental-vm-modules
when running the jest as part of yourpackage.json
test
script due to Jest's API relying on a node vm API that's still in experimental stage for esm scripts.
// package.json
"scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest"
}
- We have to tell Node our project uses ESM, so we add
"type": "module"
topackage.json
just to support ESM import statements.
This leads to a cascade of necessary changes to turn a CommonJS project to ESM:
- changing all CommonJS files with
requires("module-name")
statements toimport x from 'module-name'
- changing
module.exports
toexport default {}
- change any
.js
file extensions to.cjs
or ESM config files to.mts
.
Should be fine right? ...Not if you already have a variety of components and views importing from CJS and ESM dependencies.
At this point you have to decide whether you will isolate the folders that use ESM from the rest of the project to create a different package which can be imported into your CJS project, or if you will just convert the entire project to ESM.
- Surprise! Whenever you have
package.json
type set tomodule
, you'll encounter many errors:
ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/Users/username/projectname/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
This next one is easy; just rename jest.config.js
to jest.config.cjs
.
- Add Typescript support.
Install ts-node
and ts-jest
to help with the execution and transformation of .ts
files to .js
files before running tests.
npx npx ts-jest config:init
creates a jest.config.js
file which you can configure to your liking.
// jest.config.cjs
module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.ts$': 'ts-jest',
},
transformIgnorePatterns: ['node_modules'],
testMatch: ['**/*.test.ts'],
};
Using ts-jest
and the transform
property in jest.config.ts
will help with recognizing .ts
or .tsx
test files.
But don't use the transform
property if you're also using a ts-jest
preset! (See the module writer's disclaimer on that)
- But wait, there's more! Our test-runner complains about the
import/export
syntax:
SyntaxError: Cannot use import statement outside a module
Jest (with some support from Babel) transforms all files to CommonJS before running tests; it doesn't support import/export
statements from ES2015.
This is especially infuriating if you import a component or util function that has a direct dependency on an ESM export.
You literally will be blocked on running Jest tests on that particular component. (Even if you already configured your jest config to exclude node_modules
).
- If you by now, have decided to convert the entire project to ESM, you'll need to use
babel
to transform all.js
files to.cjs
files before running tests.
If you haven't already, you'll want to use a babel.config.js
or babel.config.json
to help with recognizing React .jsx
files and transforming them to .js
files before running tests.
Our friend is @babel/preset-env
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
'@babel/preset-react'
]
]
};
I've personally found babel/preset-typescript
to have no impact on Typescript transformation.
Side note: the maintenance-abandoned nature of CRA means installing @babel/plugin-proposal-private-property-in-object
as a devDependency or plugin to be helpful for reducing console noise.
Summary
At the end of the day, you catch yourself needing to decide when linting, transforming need to happen before tests run. I settled on:
- typechecking it first with
tsc
, OR usingts-jest
to ensure the types are correct as tests run. - transpiling with babel to CJS before running tests
If all else fails, I also had some success downgrading to Jest 27.x or 28.x instead of bashing my head on bloated config and confusing doc on 29.x.
Related Reading
BigMan73. "Answer: Jest Typescript with ES Module in node_modules error - Must use import to load ES Module", StackOverflow. Jan 4, 2023
r/node. "Jest Not Getting Support in Backend Node?"
"Jest Typescript with ES Module in node_modules error - Must use import to load ES Module", StackOverflow,
SimenB. "Support ESM versions of all pluggable modules" Mar 7, 2021
Sindre Sorhus, "Pure ESM package", 2023
Top comments (2)
I had the same issue, recently, I actually just installed Bun as a dependency of my node project and used bun test which is virtually jest. But native TS and a lot faster. Okay it’s not node or browser but if your testing some platform independent logic then it’s irrelevant and in truth it’s node is compatible.
In my opinion Jest mostly supports how code is written at Facebook, which is probably object oriented using
default
exports in vanilla JS.If you want anything else, well it's clumsy as hell.
Mocking named exports is ugly - but I won't drop tree-shaking by changing them to default exports.
Mocking exported functions instead of members of a class is also ugly.
Typing the mocks are even uglier!
The worst thing is that I can't remember how I did them; every time I copy-paste those hacks from one of the files in the project.