Spec-Zone.ru › Jest

Модули ECMAScript

внимание

Jest поставляется с экспериментальной поддержкой модулей ECMAScript (ESM).

В реализации могут быть ошибки и отсутствовать некоторые возможности. Актуальную информацию можно найти в обсуждении и по метке в трекере задач.

Также обратите внимание: API, используемые Jest для реализации поддержки ESM, по-прежнему считаются экспериментальными в Node (по состоянию на версию 18.8.0).

Предупреждения позади — вот как включить поддержку ESM в тестах.

  1. Убедитесь, что вы отключили преобразование кода, передав transform: {}, либо настроили преобразователь так, чтобы он генерировал ESM, а не CommonJS (CJS), используемый по умолчанию.

  2. Запустите 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, однако в будущем это может измениться.

  3. Кроме того, мы стараемся следовать логике node при активации «режима ESM» (например, проверяя type в файлах package.json или .mjs); подробности см. в документации Node.

  4. Чтобы считать другие расширения файлов (например, .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');
main.test.cjs

Вызов 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/wasm URI 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';
};
esm-module.mjs
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'
});
esm-module.test.mjs

Имитация модулей CJS

Для имитации модулей CJS продолжайте использовать jest.mock. См. пример ниже:

const {BrowserWindow, app} = require('electron');

// etc.

module.exports = {example};
main.cjs
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.
main.test.cjs

© 2022 Facebook, Inc.
Licensed under the MIT License.
https://jestjs.io/docs/ecmascript-modules

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API