Skip to content

Fix: jest.mock() Not Working — Module Not Being Replaced in Tests

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix jest.mock() not intercepting module calls — why mocks are ignored, how to correctly mock ES modules, default exports, named exports, and fix hoisting issues in Jest tests.

The Error

You set up a mock with jest.mock() but the real module runs anyway:

jest.mock('./api/users');
import { getUsers } from './api/users';

test('loads users', async () => {
  getUsers.mockResolvedValue([{ id: 1, name: 'Alice' }]);
  // ...but getUsers still calls the real API
});

Or the mock function is not recognized:

TypeError: getUsers.mockResolvedValue is not a function

Or a default export mock does not work:

jest.mock('./logger');
import logger from './logger';

logger.info('test'); // Still calls the real logger

Or with ES modules:

SyntaxError: Cannot use import statement inside a module

Why This Happens

jest.mock() works by intercepting require() calls at the module registry level. Several patterns break this:

  • Wrong import orderjest.mock() must be called before the module is imported. Jest hoists jest.mock() calls to the top of the file automatically, but only for require() — with ES module import syntax, the hoisting may not work as expected.
  • Mocking the wrong path — the path passed to jest.mock() must match exactly how the module is imported in the code under test.
  • Default export not mocked correctlyjest.mock('./module') auto-mocks named exports but requires explicit handling for default exports.
  • ES module mode ("type": "module") — Jest’s module mocking works differently with native ES modules.
  • Module is imported transitively — the module was already imported by another module before your mock ran.
  • Factory function not returning the mock shape — when using jest.mock('module', factory), the factory must return an object with the same shape as the module.

The deeper issue is that Jest’s mocking system was designed for CommonJS, where require() is a synchronous function call that the module registry can intercept on the fly. Babel transforms ES import statements into require() calls under the hood, which is why jest.mock() still works in most projects — but only if Babel is in the pipeline and configured to emit CommonJS. The moment you opt into native ESM via "type": "module" in package.json and skip Babel, import becomes a static, asynchronous link that Jest cannot patch retroactively. That is when you need jest.unstable_mockModule plus dynamic await import() calls.

Hoisting amplifies the confusion. babel-jest rewrites your test file so every jest.mock(...) call appears at the very top — before any import. That is what makes jest.mock('./api/users') work even when written halfway down the file. But the hoist only moves the jest.mock call itself; any variables you reference inside its factory remain at their original line. The famous “ReferenceError: Cannot access ‘mockFn’ before initialization” comes from this gap. The official workaround — prefixing variables with mock so Babel hoists them too — is undocumented in the formal API but enforced by the plugin.

The third trap is module identity. jest.mock() swaps the module exports for the module at a specific resolved path. If your code under test imports ./api/users and your test mocks ../api/users, those resolve to the same file but Jest’s module registry stores them as separate cache entries. The mock applies to one entry, the real module to the other, and your test silently runs against the real code. Always mock the path as the code under test sees it, and prefer absolute aliases over relative paths so the question does not arise.

Fix 1: Ensure jest.mock() Is Hoisted Correctly

jest.mock() calls are hoisted to the top of the file by Babel’s babel-jest transform. However, variables defined in the test file are not hoisted — only the jest.mock() call itself is:

Broken — using a variable in jest.mock() that is not yet defined:

const mockGetUsers = jest.fn();

// This line is hoisted to the top, but mockGetUsers is NOT hoisted with it
jest.mock('./api/users', () => ({
  getUsers: mockGetUsers, // undefined at hoist time — ReferenceError
}));

Fixed — use jest.fn() inline or prefix variable with mock:

// Variables starting with 'mock' are hoisted along with jest.mock()
const mockGetUsers = jest.fn();

jest.mock('./api/users', () => ({
  getUsers: mockGetUsers, // Works — babel-jest hoists 'mock*' variables
}));

// Or inline — always works:
jest.mock('./api/users', () => ({
  getUsers: jest.fn(),
}));

Then access the mock after import:

import { getUsers } from './api/users';

jest.mock('./api/users', () => ({
  getUsers: jest.fn(),
}));

test('calls getUsers', async () => {
  getUsers.mockResolvedValue([{ id: 1, name: 'Alice' }]);

  const result = await loadDashboard();

  expect(getUsers).toHaveBeenCalledOnce();
  expect(result.users).toHaveLength(1);
});

afterEach(() => {
  jest.clearAllMocks(); // Reset call counts and return values between tests
});

Fix 2: Mock Default Exports Correctly

Default exports require explicit handling in the factory function:

Broken — does not mock the default export:

jest.mock('./logger');
import logger from './logger';

logger.info('test'); // logger is {} — the real logger is not replaced

Fixed — return a default property in the factory:

jest.mock('./logger', () => ({
  default: {
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
  },
}));

import logger from './logger';

test('logs a message', () => {
  myFunction(); // internally calls logger.info(...)
  expect(logger.info).toHaveBeenCalledWith('expected message');
});

If the module has both default and named exports:

// Original module: export default logger; export { format };

jest.mock('./logger', () => ({
  __esModule: true, // Required when mocking ES modules with default exports
  default: {
    info: jest.fn(),
    warn: jest.fn(),
  },
  format: jest.fn((msg) => msg),
}));

Note: The __esModule: true flag tells Jest to treat the mock as an ES module, enabling correct default export handling. Without it, import logger from './logger' receives the entire mock object, not its .default property.

Fix 3: Fix Path Mismatch

jest.mock() intercepts the module at the path used by the code under test — not the path you use in the test file:

Example — the code under test imports from a different path:

// src/services/userService.js
import { getUsers } from '../api/users'; // Imports from '../api/users'

// Your test file:
import userService from '../services/userService';

// Wrong — mocking './api/users' but the code imports '../api/users' relative to itself
jest.mock('./api/users');

// Correct — mock the path as seen from the test file's perspective
jest.mock('../api/users');

The path in jest.mock() must resolve to the same file as the import in the code under test — use the path relative to the test file, or use an absolute alias.

With path aliases (TypeScript/webpack):

// tsconfig.json defines: "@api/*" → "src/api/*"
// Code under test uses: import { getUsers } from '@api/users';

// In your Jest config (jest.config.js), map the alias:
moduleNameMapper: {
  '^@api/(.*)$': '<rootDir>/src/api/$1',
},

// In the test — use the alias:
jest.mock('@api/users', () => ({
  getUsers: jest.fn(),
}));

Fix 4: Fix ES Module Mocking

If your project uses native ES modules ("type": "module" in package.json), jest.mock() works differently:

For projects using Babel to transform ES modules (most common):

Ensure babel-jest is configured and @babel/preset-env transforms modules to CommonJS:

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
    '@babel/preset-typescript',
  ],
};
// jest.config.js
module.exports = {
  transform: {
    '^.+\\.[tj]sx?$': 'babel-jest',
  },
  // Do NOT set "extensionsToTreatAsEsm" when using Babel for transformation
};

For native ES modules without Babel (experimental):

// jest.config.js
export default {
  extensionsToTreatAsEsm: ['.ts'],
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
  },
};
// In your test file — use jest.unstable_mockModule for native ESM
import { jest } from '@jest/globals';

// Must be called before dynamic import
jest.unstable_mockModule('./api/users', () => ({
  getUsers: jest.fn().mockResolvedValue([]),
}));

// Dynamic import AFTER the mock setup
const { getUsers } = await import('./api/users');
const { loadDashboard } = await import('./dashboard');

Fix 5: Mock Node.js Built-in Modules

Mocking built-in modules like fs, path, or crypto requires explicit module mapping:

// Mock the entire 'fs' module
jest.mock('fs');

import fs from 'fs';

test('reads a file', () => {
  fs.readFileSync.mockReturnValue('file contents');

  const result = readConfig('./config.json');

  expect(result).toBe('file contents');
  expect(fs.readFileSync).toHaveBeenCalledWith('./config.json', 'utf8');
});

Mock only specific methods with jest.spyOn:

import fs from 'fs';

test('reads a file', () => {
  const spy = jest.spyOn(fs, 'readFileSync').mockReturnValue('file contents');

  const result = readConfig('./config.json');

  expect(result).toBe('file contents');

  spy.mockRestore(); // Clean up the spy
});

Fix 6: Use jest.spyOn for Partial Mocks

When you only want to mock one method of a module while keeping the rest real:

import * as userApi from './api/users';

test('calls getUsers with correct params', async () => {
  // Mock only getUsers — all other exports remain real
  const spy = jest.spyOn(userApi, 'getUsers').mockResolvedValue([
    { id: 1, name: 'Alice' },
  ]);

  const result = await loadDashboard({ active: true });

  expect(spy).toHaveBeenCalledWith({ active: true });
  expect(result).toHaveLength(1);

  spy.mockRestore(); // Restore original implementation
});

Common Mistake: jest.spyOn requires the module to be imported as a namespace (import * as module). It does not work with destructured imports because it needs to replace a property on the module object:

// Wrong — cannot spy on a destructured import
import { getUsers } from './api/users';
jest.spyOn({ getUsers }, 'getUsers'); // Does not affect the real getUsers

// Correct
import * as userApi from './api/users';
jest.spyOn(userApi, 'getUsers').mockResolvedValue([]);

Fix 7: Reset and Restore Mocks Between Tests

Mocks that bleed between tests cause intermittent test failures:

// jest.config.js — auto-reset after each test
module.exports = {
  clearMocks: true,   // Clears mock.calls, mock.instances, mock.results
  resetMocks: true,   // Resets mock implementation (like mockReturnValue)
  restoreMocks: true, // Restores spies created with jest.spyOn
};

Or manually in each test file:

afterEach(() => {
  jest.clearAllMocks();  // Clear call history
});

afterAll(() => {
  jest.restoreAllMocks(); // Restore spies
});

Reset a specific mock’s implementation:

const mockGetUsers = jest.fn();

beforeEach(() => {
  mockGetUsers.mockReset(); // Clears calls AND removes mockReturnValue/mockImplementation
});

test('returns empty list by default', async () => {
  mockGetUsers.mockResolvedValue([]);
  // ...
});

test('returns users when available', async () => {
  mockGetUsers.mockResolvedValue([{ id: 1, name: 'Alice' }]);
  // ...
});

In Production: Incident Lens

A broken mock is not just a unit-test annoyance. It is a hole in your safety net. The test you thought verified the retry path actually hits production GitHub for a third-party API, the test you thought asserted on a logger call actually swallows every error, and the suite that gates merges to main no longer means what the team thinks it means. By the time someone notices, weeks of commits have shipped under a false signal.

Blast radius. Subtle mock failures merge with the team’s confidence intact. The CI suite is green, the coverage badge is green, and yet the code path you “tested” was never exercised. The first real signal is a runtime error in production for a scenario the unit test claimed to cover. In the worst incidents we have seen, an unmocked fetch() in a Jest run silently called a partner sandbox API thousands of times per CI run, eventually getting the team’s IP banned by the partner and breaking integration tests for a day. Every PR took twice as long until someone audited the suite.

Alert on test-suite stability metrics, not just pass/fail. Pass/fail tells you nothing about whether the test actually exercised what it claims. Track and alert on: average test duration per file (sudden jumps usually mean a mock was lost and a real network call is happening), the number of unique outbound DNS lookups during a CI run (should be near zero for unit tests), and the count of console.warn / console.error messages in test output (should be zero — every warning is a leak). A CI gate that fails the build when any of these regress catches mock-loss bugs the day they land.

Recovery. When mocks “stop working” overnight, the cause is almost always a recent change to one of three things: the Jest config (transformIgnorePatterns, moduleNameMapper, extensionsToTreatAsEsm), the Babel preset (a switch from modules: 'commonjs' to modules: false), or the consuming module’s import path (a refactor from relative paths to aliases without updating tests). Recover by reverting the most recent config change and re-running the suite. If the suite was green again, you have your culprit. Then re-apply the change incrementally with the failing tests as your guide.

Preventive design. Make ESM-vs-CJS an explicit, single-source decision in your project. Either commit to Babel-transformed CJS ("type": "commonjs" in package.json, modules: 'commonjs' in Babel, plain jest.mock) or commit to native ESM ("type": "module", jest.unstable_mockModule, dynamic await import()). Mixing the two is where 80% of mock failures originate. Align Jest’s moduleDirectories and moduleNameMapper with your bundler’s resolution so paths resolve identically in production and in tests. Add a CI step that runs your suite with the network blocked (unshare -rn on Linux, or jest-mock-extended’s network-disabling plugin) — any test that fails under network isolation has a missing mock.

Still Not Working?

Check that the module factory returns the correct shape. If jest.mock('./module', () => { ... }), the factory’s return value replaces the entire module. Missing a named export means the code under test receives undefined for that export.

Verify Jest is transforming the file. If babel-jest is not processing a file (e.g., it is in transformIgnorePatterns), jest.mock() hoisting does not work:

// jest.config.js — ensure node_modules that use ESM are transformed
transformIgnorePatterns: [
  '/node_modules/(?!(some-esm-package|another-package)/)',
],

Use jest.requireActual to partially use the real module:

jest.mock('./config', () => ({
  ...jest.requireActual('./config'), // Keep all real exports
  API_URL: 'http://localhost:3000',  // Override only this one
}));

Debug which module is actually being imported:

test('debug mock', () => {
  const mod = jest.requireMock('./api/users');
  console.log('Mock module:', mod);
  console.log('Is mock function:', jest.isMockFunction(mod.getUsers));
});

Inspect Jest’s module registry directly. Inside a beforeAll, dump require.cache (CJS) or run jest.getModuleRegistry?.() to see exactly which modules Jest considers loaded. If the module under test appears with a path different from the one you mocked, your mock targets the wrong identity. Fix by switching both your code under test and your jest.mock(...) call to the same absolute alias (@/api/users) instead of mixing relative paths.

Confirm resetModules is not stripping your mock. Calling jest.resetModules() clears the module registry and forces a re-evaluation. If you call it after jest.mock(...) is hoisted, the next import re-runs the module without the mock context. Move jest.mock calls inside beforeEach using jest.doMock(...) (which is not hoisted) when you need to combine resetModules with mocking — that gives you predictable ordering.

Watch for double-mocking by automock. If you set automock: true in jest.config.js, every module is auto-mocked unless explicitly opted out with jest.unmock(...). A manual jest.mock('./api/users', factory) then runs against an already-mocked target, and the factory’s return value may not be what your test reads. Disable automock and mock explicitly per test file.

For related testing issues, see Fix: Jest Test Timeout, Fix: Jest Cannot Find Module, Fix: Jest ESM Error, and Fix: Jest Module Mock Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles