Fix: Jest Module Mock Not Working — jest.mock() Has No Effect
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Jest module mocks not working — hoisting behavior, ES module mocks, factory functions, mockReturnValue vs implementation, and clearing mocks between tests.
The Problem
jest.mock() is called but the original module is still used:
import { fetchUser } from './api';
jest.mock('./api');
test('loads user', async () => {
fetchUser.mockResolvedValue({ name: 'Alice' });
// fetchUser still makes a real HTTP request — mock has no effect
});Or a mock is set up but returns undefined instead of the mocked value:
jest.mock('./database', () => ({
query: jest.fn(),
}));
// In the test:
const { query } = require('./database');
query.mockReturnValue([{ id: 1 }]); // Set up mock
const result = await userService.getUsers();
// result is empty — query returned undefined, not [{ id: 1 }]Or TypeScript shows an error on the mock:
Property 'mockResolvedValue' does not exist on type '() => Promise<User>'
ts(2339)Why This Happens
Jest’s mock system has several non-obvious behaviors that interact with each other in surprising ways.
The core mechanism is hoisting. When you call jest.mock('./api') anywhere in a test file, Babel’s babel-jest transform lifts that call to the very top of the file, above all import statements. This means the mock is registered before the module is loaded. But this hoisting only works with CommonJS modules (or code that Babel transpiles to CommonJS). If you are running native ESM — either through Node.js --experimental-vm-modules or a bundler that preserves ES imports — the hoisting doesn’t happen. Your import runs first, loads the real module, and the jest.mock() call comes too late.
The second major trap is the factory function scope. When you pass a factory to jest.mock('./api', () => ({ ... })), that factory runs in a sandboxed environment. Variables declared outside the factory (in the test file’s top-level scope) are not accessible inside it. If you create const mockFn = jest.fn() outside the factory and reference mockFn inside, you get a ReferenceError because the factory executes before the outer scope is initialized. The exception is identifiers that start with mock — Jest allows those by convention.
Other failure causes:
- Module caching — Jest caches modules per test file. If a module is required before the mock is set up (in module initialization code), the cached version is used.
- Wrong import style — if the module under test uses
require()internally but you mock it with ESM import syntax, or vice versa, the mock may not intercept the call. - Mock not cleared between tests — mock state (call counts, return values) persists across tests in the same file unless explicitly cleared.
Diagnostic Timeline
When jest.mock() appears to have no effect, here is how to isolate the cause step by step.
Minute 0 — Confirm the mock is actually not working. Add a console.log inside the mock factory to verify it executes:
jest.mock('./api', () => {
console.log('MOCK FACTORY EXECUTED');
return { fetchUser: jest.fn() };
});If you see the log, the mock factory runs but something else is wrong (wrong module path, the code under test imports a different path). If you don’t see the log, the mock is never applied — likely an ESM or hoisting issue.
Minute 2 — Check import vs require. Look at the module under test (not the test file). If it uses require('./api'), the mock works with standard jest.mock(). If it uses import { fetchUser } from './api' and your project uses native ESM (check jest.config for transform: {} or type: "module" in package.json), jest.mock() hoisting doesn’t apply. You need jest.unstable_mockModule() instead.
Minute 5 — Verify the mock factory returns the correct shape. The factory must return an object that matches the module’s export structure. A module with export function fetchUser() needs a factory returning { fetchUser: jest.fn() }. A module with export default function() needs { default: jest.fn(), __esModule: true }. Missing the __esModule: true flag for default exports is a common oversight.
Minute 8 — Check jest.config for moduleNameMapper. If moduleNameMapper remaps the module path, jest.mock() must use the original path (the one in the source code), not the mapped path. Jest resolves the path from the mock call to the same file that the import resolves to. If those don’t match, the mock applies to a different module.
Minute 10 — Run with --no-cache. Jest caches compiled modules. A stale cache can cause mocks to be ignored. Run npx jest --no-cache to eliminate caching as a factor.
Minute 12 — Check for import-time side effects. If the module under test runs code at import time that calls the function you want to mock, that code executes before your test’s beforeEach sets up mock return values. The mock exists (it’s a jest.fn()), but it returns undefined because you haven’t configured it yet. Move the return value setup to the factory or restructure the module.
Fix 1: Ensure jest.mock() Is at the Top Level
jest.mock() must be at the top level of the file (not inside functions, beforeEach, or describe blocks) for Babel hoisting to work:
// WRONG — inside describe() — hoisting won't move this above imports
describe('UserService', () => {
jest.mock('./api'); // Runs after imports — mock not applied
// ...
});
// WRONG — inside beforeEach — too late
beforeEach(() => {
jest.mock('./api'); // Not hoisted, runs at runtime after module is already imported
});
// CORRECT — at top level, outside all blocks
jest.mock('./api'); // Babel hoists this above import statements
import { fetchUser } from './api';
describe('UserService', () => {
test('...', () => { /* ... */ });
});Babel transforms this to:
// What Babel actually generates (simplified):
jest.mock('./api');
const api_1 = require('./api');
// Mock is applied before the requireFix 2: Use jest.fn() in the Factory Function
When using a factory function with jest.mock(), create jest.fn() instances inside the factory — not outside it:
// WRONG — referencing outer variable in factory
const mockQuery = jest.fn(); // Created in outer scope
jest.mock('./database', () => ({
query: mockQuery, // Can't access outer scope from factory — ReferenceError
}));// CORRECT — create jest.fn() inside the factory
jest.mock('./database', () => ({
query: jest.fn(),
connect: jest.fn().mockResolvedValue(true),
disconnect: jest.fn(),
}));
// Access the mock in tests
const { query } = require('./database');
test('getUsers calls query', async () => {
query.mockResolvedValue([{ id: 1, name: 'Alice' }]);
const users = await userService.getUsers();
expect(query).toHaveBeenCalledWith('SELECT * FROM users');
expect(users).toHaveLength(1);
});If you need a reference to the mock function outside the factory, use jest.mocked() or a require after the mock:
jest.mock('./database', () => ({
query: jest.fn(),
}));
// Import after jest.mock() — gets the mocked version
import { query } from './database';
// OR use require() in tests
const db = require('./database');
test('...', () => {
db.query.mockReturnValue([]);
});Fix 3: Mock Default Exports Correctly
Default exports and named exports require different mock syntax:
// Module: utils/logger.js
export default function log(message) {
console.log(message);
}
export const warn = (msg) => console.warn(msg);// Mocking default export
jest.mock('./utils/logger', () => {
return {
default: jest.fn(), // 'default' key for default export
warn: jest.fn(), // Named export
__esModule: true, // Required to tell Jest this is an ES module
};
});
// OR more commonly — mock the entire module's default as a jest.fn()
jest.mock('./utils/logger', () => ({
__esModule: true,
default: jest.fn(),
}));
// Access in tests
import log from './utils/logger';
// log is now the jest.fn() from the mock
test('calls logger', () => {
doSomething();
expect(log).toHaveBeenCalledWith('expected message');
});Class mocks:
// Original: services/EmailService.js
export class EmailService {
async send(to, subject, body) {
// Real email sending logic
}
}
// Mock the class
jest.mock('./services/EmailService', () => {
return {
EmailService: jest.fn().mockImplementation(() => ({
send: jest.fn().mockResolvedValue({ messageId: 'test-123' }),
})),
};
});
import { EmailService } from './services/EmailService';
test('sends email', async () => {
const service = new EmailService();
await service.send('[email protected]', 'Hello', 'World');
expect(service.send).toHaveBeenCalledTimes(1);
});Fix 4: Fix TypeScript Type Errors on Mocked Functions
TypeScript doesn’t know a function is a Jest mock — use jest.mocked():
// Without type fix — TypeScript error
import { fetchUser } from './api';
jest.mock('./api');
test('...', () => {
fetchUser.mockResolvedValue({ name: 'Alice' }); // TS Error: Property 'mockResolvedValue' does not exist
});// CORRECT — use jest.mocked() to get typed mock
import { fetchUser } from './api';
jest.mock('./api');
test('...', async () => {
// jest.mocked() wraps the function with Jest's mock type information
jest.mocked(fetchUser).mockResolvedValue({ id: 1, name: 'Alice' });
const user = await getUser(1);
expect(user.name).toBe('Alice');
});Cast with as jest.Mock (older approach):
(fetchUser as jest.Mock).mockResolvedValue({ name: 'Alice' });
// Or extract for readability
const mockFetchUser = fetchUser as jest.Mock;
mockFetchUser.mockResolvedValue({ name: 'Alice' });With TypeScript strict mode, prefer jest.mocked() — it preserves the original function’s type signature while adding mock methods.
Fix 5: Clear and Reset Mocks Between Tests
Mocks accumulate state (call counts, return values) between tests unless explicitly cleared:
jest.mock('./api', () => ({
fetchUser: jest.fn(),
}));
const { fetchUser } = require('./api');
// BAD — mocks accumulate state between tests
test('test 1', () => {
fetchUser.mockReturnValue({ name: 'Alice' });
// ...
});
test('test 2', () => {
// fetchUser still returns { name: 'Alice' } from test 1
// and call count includes calls from test 1
expect(fetchUser).not.toHaveBeenCalled(); // FAILS — called in test 1
});// GOOD — clear mocks between tests
beforeEach(() => {
jest.clearAllMocks();
// Clears: call counts, instances, results, and mock.calls
// Does NOT reset implementation (mockReturnValue, mockImplementation)
});
// OR reset implementation too
beforeEach(() => {
jest.resetAllMocks();
// Clears everything + resets implementation to undefined
});
// OR restore original implementation (for jest.spyOn)
afterEach(() => {
jest.restoreAllMocks();
// Restores original implementation for spies
});Configure globally in jest.config.js:
// jest.config.js
module.exports = {
clearMocks: true, // Equivalent to jest.clearAllMocks() before each test
resetMocks: false, // Don't reset implementations automatically
restoreMocks: false, // Don't restore spies automatically
};Fix 6: Use jest.spyOn for Partial Mocking
When you only want to mock specific methods of a module (not the entire module):
import * as api from './api';
test('mocks a specific function', async () => {
// Replace just fetchUser — other exports remain real
const spy = jest.spyOn(api, 'fetchUser').mockResolvedValue({ name: 'Alice' });
const result = await api.fetchUser(1);
expect(result.name).toBe('Alice');
expect(spy).toHaveBeenCalledWith(1);
// Restore original after test
spy.mockRestore();
});
// OR use afterEach with restoreAllMocks
afterEach(() => {
jest.restoreAllMocks();
});Spy on class methods:
import { UserService } from './UserService';
test('mocks service method', async () => {
const service = new UserService();
jest.spyOn(service, 'findById').mockResolvedValue({
id: 1, name: 'Alice',
});
const user = await service.findById(1);
expect(user.name).toBe('Alice');
});Fix 7: Handle ES Module Mocking
With native ESM (without Babel transpilation), jest.mock() hoisting doesn’t work. Use jest.unstable_mockModule() for native ESM:
// With native ESM and "--experimental-vm-modules"
// jest.config.js
// { "transform": {} } // No Babel transform
// In test file
const { fetchUser } = await import('./api');
// WRONG — jest.mock() won't hoist in native ESM
jest.mock('./api'); // Doesn't work with native ESM
// CORRECT — use unstable_mockModule (Promise-based)
await jest.unstable_mockModule('./api', () => ({
fetchUser: jest.fn().mockResolvedValue({ name: 'Alice' }),
}));
// Dynamic import AFTER the mock is set up
const { fetchUser } = await import('./api');
test('mocks fetchUser', async () => {
const result = await fetchUser(1);
expect(result.name).toBe('Alice');
});Recommended: use Babel or ts-jest to avoid native ESM complexity — see Fix: Jest ESM Error for details on configuring Babel with Jest.
Still Not Working?
Check if the module has side effects on import — if the module runs code on import that calls the function you’re trying to mock, the mock isn’t set up yet:
// problematic-module.js
import { track } from './analytics';
track('module-loaded'); // Runs at import time, before jest.mock can intercept
// Solution: restructure to delay the call, or use jest.mock with a factory
// that prevents the side effectVerify mock is applied to the right path — the path in jest.mock() must exactly match the import path in the module under test (not the test file):
// Module under test (userService.js) imports:
import { query } from '../db/database'; // This path
// Mock must use the SAME path relative to the module under test
// From the test file's perspective:
jest.mock('../db/database'); // Must resolve to the same fileUse jest.mock() with the module path as seen from the test file — Jest resolves both to the same absolute path.
Manual mock with __mocks__ directory — create __mocks__/api.js next to api.js. Jest uses it automatically for manual mocks:
src/
├── api.js ← Original
├── __mocks__/
│ └── api.js ← Automatic mock (used when jest.mock('./api') is called)
└── userService.test.jsjest.mock() path is case-sensitive on Linux — on macOS and Windows, jest.mock('./Api') and jest.mock('./api') resolve to the same file. On Linux CI runners, they don’t. If mocks work locally but fail in CI, check for case mismatches in the mock path.
Mock doesn’t intercept re-exported functions — if module A re-exports a function from module B (export { fetchUser } from './api'), mocking module A doesn’t affect the original in module B. Mock module B directly, or mock module A with a factory that replaces the re-export.
Vitest migration gotcha — if you’re migrating from Jest to Vitest, vi.mock() has slightly different hoisting behavior and doesn’t require __esModule: true for default exports. Mocks that worked in Jest may need adjustments for Vitest’s native ESM handling.
For related Jest issues, see Fix: Jest Fake Timers Not Working, Fix: Jest ESM Error, Fix: Jest Cannot Find Module, and Fix: Jest 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.