Jest is one of the most popular testing frameworks, but it hasn’t really kept up with the growing support for native JavaScript modules (ESM) in the developer community. A fresh install of Jest will simply not work with native modules.

jest test failure message

However, Jest does have experimental support for ES modules. Combined with the native module support in recent versions of Node, it’s possible to use Jest with a native ESM-based project.

Loading native modules

The first issue is that Jest can’t understand ES modules by default. You’ll need to do two things to resolve this: 1) prevent Jest from trying to transform ESM code to CommonJS, and 2) configure Node to allow Jest to load ESM code.To prevent Jest from trying to transform ESM code, disable transforms for ES modules. Transforms for all files can be disabled by setting Jest’s transform property to an empty value:

"jest": {
  "transform": {}
}

For Jest to be able to parse ES modules without an external transformer (e.g., babel), start Node with the –experimental-vm-modules flag. This can be done by changing how Jest is started by the npm “test” script:

"scripts": {
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
}

At this point, Jest will be able to load tests written in ES modules. Progress! However, simply loading test modules will only get you so far. Unit tests also typically involve a lot of mocking, which requires some additional work when ESM with Jest.

Mocking

Normally, modules are mocked in Jest using jest.mock(). However, due to how the ESM loader works, this function can’t be used for native ES modules. The Jest maintainers plan to support mocking for native modules, but there is no official API yet.

Luckily there is an in-development API that is capable of mocking ES modules, unstable_mockModule, that’s been available since Jest 27.0.0. There are a couple of differences between it and the standard mock() function. First, while mock can take an optional factory function as its second argument, this argument is required for unstable_mockModule. A side effect of this requirement is that mocks defined in __mocks__ directories will no longer be automatically loaded. Mock modules will now need to be manually loaded:

jest.unstable_mockModule('fs', async function () {
  return import('./__mocks__/fs.js');
});

Note that the factory function here is using import rather than jest.requireActual. This is because the fs mock is an ES module (assuming the project type is “module”) and ES modules must be imported using import.

The second difference from mock() is that modules with resources being mocked by unstable_mockModule must be dynamically imported. When working with CommonJS modules, or with ES modules that are being transpiled to CommonJS by babel, Jest automatically hoists mock calls so that the mocking will take place before any imports in a test module. That doesn’t work with native ES modules, so for mocking to work, any calls to unstable_mockModule must explicitly happen before any modules that use the mocked resources are imported. Unit tests using this pattern would look like:

jest.unstable_mockModule('fs', function () {
  return import('./__mocks__/fs.js');
});

describe('util', function () {
  it('loads JSON  files', async function () {
    // import the module being tested, which uses the mocked resource
    const { loadJson } = await import('./util.js');
    const data = loadJson('foo.json');
    expect(data).toEqual({ items: ['one', 'two', 'three'] });
  });
});

Conclusion

Although Jest’s official ESM support is still experimental, it is usable today. With some minor adjustments to standard workflows, it’s possible to use Jest to test projects based on modern, native ES modules.