Skip to content

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

FixDevs ·

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.

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' }]);
  // ...
});

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));
});

For related testing issues, see Fix: Jest Test Timeout and Fix: Jest Cannot Find Module.

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