Модули ECMAScript
Jest ships with experimental support for ECMAScript Modules (ESM).
The implementation may have bugs and lack features. For the latest status check out the issue and the label on the issue tracker.
Also note that the APIs Jest uses to implement ESM support are still considered experimental by Node (as of version 18.8.0).
With the warnings out of the way, this is how you activate ESM support in your tests.
-
Ensure you either disable code transforms by passing
transform: {}or otherwise configure your transformer to emit ESM rather than the default CommonJS (CJS). -
Execute
nodewith--experimental-vm-modules, e.g.node --experimental-vm-modules node_modules/jest/bin/jest.jsorNODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jestetc.On Windows, you can use
cross-envto be able to set environment variables.If you use Yarn, you can use
yarn node --experimental-vm-modules $(yarn bin jest). This command will also work if you use Yarn Plug'n'Play.If your codebase includes ESM imports from
*.wasmfiles, you do not need to pass--experimental-wasm-modulestonode. Current implementation of WebAssembly imports in Jest relies on experimental VM modules, however, this may change in the future. -
Beyond that, we attempt to follow
node's logic for activating "ESM mode" (such as looking attypeinpackage.jsonor.mjsfiles), see their docs for details. -
If you want to treat other file extensions (such as
.jsxor.ts) as ESM, please use theextensionsToTreatAsEsmoption.
Различия между ESM и CommonJS
Most of the differences are explained in Node's documentation, but in addition to the things mentioned there, Jest injects a special variable into all executed files - the jest object. To access this object in ESM, you need to import it from the @jest/globals module or use import.meta.
import {jest} from '@jest/globals';
jest.useFakeTimers();
// etc.
// alternatively
import.meta.jest.useFakeTimers();
// jest === import.meta.jest => true
require() of ESM
On Node v24.9 and later, Jest supports require()-ing an ES module from CJS code, mirroring Node's own require(esm).
const {value, default: defaultExport} = require('./esm-module.mjs');
Calling require() on an ESM file with top-level await (or whose graph contains TLA) throws ERR_REQUIRE_ASYNC_MODULE. Use await import(...) for those files.
Packages resolve through the require and module-sync conditions, as they do in Node. A package that exposes its ESM entry point under module-sync can therefore be require()d, and one that exposes it only under import cannot - Node refuses that too, with ERR_PACKAGE_PATH_NOT_EXPORTED. A module-sync entry point whose graph contains top-level await throws ERR_REQUIRE_ASYNC_MODULE.
jest.mock does not apply when the resolved file is ESM - jest.mock is for CJS targets. To mock an ESM file you require(), register the mock via jest.unstable_mockModule (the mock applies to transitive dependencies the loaded ESM imports).
On Node versions older than v24.9, require() of an ESM file still throws ERR_REQUIRE_ESM.
Divergences from Node
Jest's module system diverges from Node's in a few places:
- In a graph that mixes ESM and CJS, the CJS dependencies execute while the graph is built, so a CJS module can run earlier relative to its ESM siblings than it would in Node.
- The named exports of a CJS module imported from ESM are a superset of Node's: keys present on
module.exportsafter evaluation are exposed in addition to what static analysis finds. - Writes to and deletes from
require.cacheare silently ignored. - The static members of
require('module')(such asModule._resolveFilename) come from the host Node, not from Jest's module system. require()of an ES module that is part of a graph currently being loaded throwsERR_REQUIRE_CYCLE_MODULEeven when the required module is not an ancestor of the requiring module.- The
'module.exports'named export of an imported CJS module is exposed on every Node version, including versions older than v23 where Node itself does not provide it. - Importing JSON without
with {type: 'json'}emits a warning instead of throwing. This becomes an error in a future major version. - An
application/wasmdata: URI requires the;base64parameter and reports a descriptive error without it, where Node hands the percent-decoded text to WebAssembly and fails withCompileError. - A bare core specifier with a query or fragment (
import 'fs?q') throwsERR_UNKNOWN_BUILTIN_MODULE, where Node treats the whole string as a package name and fails withERR_MODULE_NOT_FOUND. Thenode:-prefixed form throws the same error in both. - Stack traces show file paths, not
file://URLs.
Module mocking in ESM
Since ESM evaluates static import statements before looking at the code, the hoisting of jest.mock calls that happens in CJS won't work for ESM. To mock modules in ESM, you need to use require or dynamic import() after jest.mock calls to load the mocked modules - the same applies to modules which load the mocked modules.
ESM mocking is supported through jest.unstable_mockModule. As the name suggests, this API is still work in progress, please follow this issue for updates.
The usage of jest.unstable_mockModule is essentially the same as jest.mock with two differences: the factory function is required and it can be sync or async:
import {jest} from '@jest/globals';
jest.unstable_mockModule('node:child_process', () => ({
execSync: jest.fn(),
// etc.
}));
const {execSync} = await import('node:child_process');
// etc.
Module unmocking in ESM
export default () => {
return 'default';
};
export const namedFn = () => {
return 'namedFn';
};
import {jest, test} from '@jest/globals';
test('test esm-module', async () => {
jest.unstable_mockModule('./esm-module.js', () => ({
default: () => 'default implementation',
namedFn: () => 'namedFn implementation',
}));
const mockModule = await import('./esm-module.js');
console.log(mockModule.default()); // 'default implementation'
console.log(mockModule.namedFn()); // 'namedFn implementation'
jest.unstable_unmockModule('./esm-module.js');
const originalModule = await import('./esm-module.js');
console.log(originalModule.default()); // 'default'
console.log(originalModule.namedFn()); // 'namedFn'
/* !!! WARNING !!! Don`t override */
jest.unstable_mockModule('./esm-module.js', () => ({
default: () => 'default override implementation',
namedFn: () => 'namedFn override implementation',
}));
const mockModuleOverride = await import('./esm-module.js');
console.log(mockModuleOverride.default()); // 'default implementation'
console.log(mockModuleOverride.namedFn()); // 'namedFn implementation'
});
Mocking CJS modules
For mocking CJS modules, you should continue to use jest.mock. See the example below:
const {BrowserWindow, app} = require('electron');
// etc.
module.exports = {example};
import {createRequire} from 'node:module';
import {jest} from '@jest/globals';
const require = createRequire(import.meta.url);
jest.mock('electron', () => ({
app: {
on: jest.fn(),
whenReady: jest.fn(() => Promise.resolve()),
},
BrowserWindow: jest.fn().mockImplementation(() => ({
// partial mocks.
})),
}));
const {BrowserWindow} = require('electron');
const exported = require('./main.cjs');
// alternatively
const {BrowserWindow} = (await import('electron')).default;
const exported = await import('./main.cjs');
// etc.