Fix: jest.mock() Not Working — Module Not Being Replaced in Tests
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 functionOr a default export mock does not work:
jest.mock('./logger');
import logger from './logger';
logger.info('test'); // Still calls the real loggerOr with ES modules:
SyntaxError: Cannot use import statement inside a moduleWhy This Happens
jest.mock() works by intercepting require() calls at the module registry level. Several patterns break this:
- Wrong import order —
jest.mock()must be called before the module is imported. Jest hoistsjest.mock()calls to the top of the file automatically, but only forrequire()— with ES moduleimportsyntax, 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 correctly —
jest.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 replacedFixed — 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: trueflag 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.defaultproperty.
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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Jest Setup File Not Working — setupFilesAfterFramework Not Running or Globals Not Applied
How to fix Jest setup file issues — setupFilesAfterFramework vs setupFiles, global mocks not applying, @testing-library/jest-dom matchers, module mocking in setup, and TypeScript setup files.
Fix: Jest Async Test Timeout — Exceeded 5000ms or Test Never Resolves
How to fix Jest async test timeouts — missing await, unresolved Promises, done callback misuse, global timeout configuration, fake timers, and async setup/teardown issues.
Fix: Jest Coverage Not Collected — Files Missing from Coverage Report
How to fix Jest coverage not collecting all files — collectCoverageFrom config, coverage thresholds, Istanbul ignore comments, ts-jest setup, and Babel transform issues.
Fix: Jest Fake Timers Not Working — setTimeout and setInterval Not Advancing
How to fix Jest fake timers not working — useFakeTimers setup, runAllTimers vs advanceTimersByTime, async timers, React testing with act(), and common timer test mistakes.