Модули ECMAScript
Jest поставляется с экспериментальной поддержкой модулей ECMAScript (ESM).
В реализации могут быть ошибки и отсутствовать некоторые возможности. Актуальную информацию можно найти в обсуждении и по метке в трекере задач.
Также обратите внимание: API, используемые Jest для реализации поддержки ESM, по-прежнему считаются экспериментальными в Node (по состоянию на версию 18.8.0).
Предупреждения позади — вот как включить поддержку ESM в тестах.
-
Убедитесь, что вы отключили преобразование кода, передав
transform: {}, либо настроили преобразователь так, чтобы он генерировал ESM, а не CommonJS (CJS), используемый по умолчанию. -
Запустите
nodeс--experimental-vm-modules, напримерnode --experimental-vm-modules node_modules/jest/bin/jest.jsилиNODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jestи т. д.В Windows можно использовать
cross-env, чтобы иметь возможность задавать переменные среды.Если вы используете Yarn, можно воспользоваться командой
yarn node --experimental-vm-modules $(yarn bin jest). Эта команда также работает с Yarn Plug'n'Play.Если в вашей кодовой базе есть импорты ESM из файлов
*.wasm, передавать--experimental-wasm-modulesвnodeне нужно. Текущая реализация импорта WebAssembly в Jest полагается на экспериментальные модули VM, однако в будущем это может измениться. -
Кроме того, мы стараемся следовать логике
nodeпри активации «режима ESM» (например, проверяяtypeв файлахpackage.jsonили.mjs); подробности см. в документации Node. -
Чтобы считать другие расширения файлов (например,
.jsxили.ts) файлами ESM, используйте параметрextensionsToTreatAsEsm.
Различия между ESM и CommonJS
Большинство различий описано в документации Node, но, помимо упомянутого там, Jest внедряет во все исполняемые файлы специальную переменную — объект jest. Чтобы получить доступ к этому объекту в ESM, его нужно импортировать из модуля @jest/globals или использовать import.meta.
import {jest} from '@jest/globals';
jest.useFakeTimers();
// etc.
// alternatively
import.meta.jest.useFakeTimers();
// jest === import.meta.jest => true
require() модулей ESM
Начиная с Node v24.9, Jest поддерживает вызов require() для ES-модуля из кода CJS, следуя примеру собственного require(esm) в Node.
const {value, default: defaultExport} = require('./esm-module.mjs');
Вызов require() для файла ESM с конструкцией await на верхнем уровне (или в графе зависимостей которого есть TLA) приводит к ошибке ERR_REQUIRE_ASYNC_MODULE. Для таких файлов используйте await import(...). Та же ошибка возникает, если настроенный преобразователь или резолвер поддерживает только асинхронную работу: require() не может его дождаться.
Пакеты разрешаются с условиями require и module-sync, как и в Node. Поэтому пакет, предоставляющий точку входа ESM через module-sync, можно require(), а пакет, предоставляющий её только через import, — нельзя: Node также откажет в этом с ошибкой ERR_PACKAGE_PATH_NOT_EXPORTED. Точка входа module-sync, в графе зависимостей которой есть await на верхнем уровне, приводит к ошибке ERR_REQUIRE_ASYNC_MODULE.
jest.mock не применяется, если разрешённый файл — ESM: jest.mock предназначен для целей CJS. Чтобы имитировать файл ESM, необходимо require(), зарегистрировав имитацию через jest.unstable_mockModule: она применяется как к файлу, который вы require(), так и к импортируемым им зависимостям ESM. jest.requireActual обходит её, как и в случае имитаций CJS.
В версиях Node до v24.9 вызов require() для файла ESM по-прежнему приводит к ошибке ERR_REQUIRE_ESM.
Отличия от Node
Система модулей Jest в нескольких аспектах отличается от системы Node:
- В графе, сочетающем ESM и CJS, зависимости CJS выполняются после разрешения графа, но до выполнения тел модулей ESM. Поэтому модуль CJS может выполниться раньше своих соседей ESM, чем это произошло бы в Node. Как и в Node, в версиях Node 24.9 и выше граф с ошибкой разрешения или атрибутов импорта не приводит к выполнению какого-либо кода; устаревший загрузчик в более ранних версиях по-прежнему выполняет зависимость CJS во время связывания, до появления последующей ошибки графа.
- Метаданные автомоков собираются после разрешения импортирующего графа, по одному имитируемому модулю за раз: при каждом решении загружается реальный модуль, поэтому ошибка загрузки в собственном графе одного из имитируемых модулей не отменяет предшествующую загрузку метаданных.
- Именованные экспорты модуля CJS, импортированного из ESM, представляют собой надмножество именованных экспортов Node: после вычисления также экспортируются ключи, присутствующие в
module.exports, помимо найденных статическим анализом. - Запись в
require.cacheи удаление из него молча игнорируются. - Статические элементы
require('module')(например,Module._resolveFilename) поступают из Node-хоста, а не из системы модулей Jest. -
require()ES-модуля, входящего в загружаемый граф, приводит к ошибкеERR_REQUIRE_CYCLE_MODULE, даже если требуемый модуль не является предком запрашивающего модуля. - Именованный экспорт
'module.exports'импортированного модуля CJS доступен во всех версиях Node, в том числе в версиях до v23, в которых сам Node его не предоставляет. - Импорт JSON без
with {type: 'json'}вызывает предупреждение, а не ошибку. В будущей основной версии это станет ошибкой. - Для
application/wasmURI data: требуется параметр;base64; если он отсутствует, выводится понятная ошибка. В Node текст с декодированными процентными последовательностями передаётся в WebAssembly, что приводит к ошибкеCompileError. - Указание встроенного модуля с запросом или фрагментом (
import 'fs?q') приводит к ошибкеERR_UNKNOWN_BUILTIN_MODULE, тогда как Node считает всю строку именем пакета и выдаёт ошибкуERR_MODULE_NOT_FOUND. Форма с префиксомnode:приводит к той же ошибке в обеих системах. - Импорты исходной фазы (
import source/import.source(), используемые для WebAssembly) приводят к ошибкеERR_SOURCE_PHASE_NOT_DEFINED: Node прикрепляет скомпилированныйWebAssembly.Moduleк целевому объекту через внутренний API, которыйnode:vmне предоставляет, поэтому Jest не может предоставить исходный объект. - В трассировках стека указываются пути к файлам, а не URL
file://.
Имитация модулей в ESM
Поскольку ESM обрабатывает статические инструкции import до выполнения кода, поднятие вызовов jest.mock, применяемое в CJS, не работает для ESM. Чтобы имитировать модули в ESM, нужно использовать require или динамический import() после вызовов jest.mock для загрузки имитируемых модулей. То же касается модулей, которые загружают имитируемые модули.
Имитация ESM поддерживается через jest.unstable_mockModule. Как следует из названия, этот API всё ещё находится в разработке; следите за этим обсуждением, чтобы получать обновления.
В Node 24.9 и выше автоматическая имитация и файлы ручных имитаций __mocks__ также применяются к ESM: автомок создаётся на основе пространства имён реального модуля, а соседний файл __mocks__ заменяет его для статических импортов, динамических import() и require(). Это работает только для графов, вычисляемых синхронно: для модуля, граф которого требует асинхронного вычисления (await на верхнем уровне), или проекта, резолвер либо преобразователь которого работает только асинхронно или предоставляет отдельные синхронные и асинхронные обработчики, по-прежнему требуется jest.unstable_mockModule.
Использование jest.unstable_mockModule по сути такое же, как jest.mock, но есть два отличия: фабричная функция обязательна и может быть синхронной или асинхронной:
import {jest} from '@jest/globals';
jest.unstable_mockModule('node:child_process', () => ({
execSync: jest.fn(),
// etc.
}));
const {execSync} = await import('node:child_process');
// etc.
Отмена имитации модулей в 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'
});
Имитация модулей CJS
Для имитации модулей CJS продолжайте использовать jest.mock. См. пример ниже:
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.
© 2022 Facebook, Inc.
Licensed under the MIT License.
https://jestjs.io/docs/ecmascript-modules