Skip to content

Mocking ES Modules

Mocking modules that use import/export syntax works slightly differently than CommonJS require/module.exports. This lesson covers the specific patterns needed for ES modules.

Mocking Named and Default Exports

When a module uses ES module syntax, Jest (via Babel's CommonJS interop) still lets jest.mock() work the same way, but the shape of the mock factory needs to account for default exports specifically, since import Something from './module' maps to a default property under the hood.

Named exports (import { getUser } from './api') map directly to keys on the mocked module object, just like CommonJS.

// api.js
export function getUser(id) { /* real fetch logic */ }
export default function getConfig() { /* real fetch logic */ }

// api.test.js
jest.mock('./api', () => ({
  getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Ada' }),
  default: jest.fn().mockReturnValue({ env: 'test' }),
}));

import getConfig, { getUser } from './api';

The factory's default key corresponds to the module's default export; other keys map to named exports.

Mocking a Single Named Export with spyOn

import * as api from './api';

jest.spyOn(api, 'getUser').mockResolvedValue({ id: 1, name: 'Ada' });
  • Importing * as api gives you an object you can jest.spyOn() for a single named export.
  • This avoids mocking the entire module when only one export needs to change.
  • This pattern requires Babel's ESM-to-CommonJS interop (the default in most Jest + Babel setups).
  • Native ESM support in Jest (via --experimental-vm-modules) has some restrictions around mutating imports this way.

Mocking ES Modules Cheat Sheet

Common patterns for mocking import/export syntax.

Goal Pattern
Mock a named export jest.mock('./api', () => ({ getUser: jest.fn() }))
Mock a default export jest.mock('./api', () => ({ default: jest.fn() }))
Spy on one named export jest.spyOn(api, 'getUser') after import * as api
Keep other exports real Spread jest.requireActual('./api') in the factory
Native ESM mode Run Jest with NODE_OPTIONS=--experimental-vm-modules

Partially Mocking an ES Module

Just like CommonJS partial mocks, you can spread jest.requireActual() inside the factory to keep most named exports real while overriding just one or two.

jest.mock('./api', () => {
  const actual = jest.requireActual('./api');
  return {
    ...actual,
    getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Ada' }),
  };
});

Native ESM Support Considerations

Jest has experimental native ESM support that avoids Babel entirely, but it has stricter rules: modules are read-only, so patterns like jest.spyOn(api, 'getUser') on a live binding may not work the same way. Most projects still rely on Babel's CommonJS interop for the smoothest mocking experience.

  • Babel-based ESM (via @babel/preset-env) is the most common and best-supported setup for mocking.
  • Native ESM (--experimental-vm-modules) trades some mocking flexibility for closer spec compliance.
  • Check your project's Babel/transform configuration before assuming a mocking pattern will work.

Common Mistakes

  • Forgetting the default key when mocking a module with a default export.
  • Trying to reassign an imported binding directly instead of using jest.mock() or jest.spyOn().
  • Assuming native ESM mode supports every mocking trick that Babel-transformed CommonJS supports.
  • Mocking an entire module when a single jest.spyOn() on one named export would be simpler.

Key Takeaways

  • ES module mocking works through the same jest.mock() API, with a default key for default exports.
  • import * as module plus jest.spyOn() lets you mock a single named export.
  • Partial mocking with jest.requireActual() works the same as it does for CommonJS.
  • Babel-based ESM interop is generally the most flexible setup for mocking today.

Pro Tip

If a default export mock isn't taking effect, double-check your factory includes a default key — this is the single most common mistake when mocking ES modules in Jest.