Skip to content

Fix: Jest Module Mock Not Working — jest.mock() Has No Effect

FixDevs ·

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:

  • Hoistingjest.mock() calls are automatically hoisted to the top of the file by Babel’s babel-jest transform. If the module is imported before the mock is applied (or hoisting fails), the real module is used.
  • ES modules bypass hoisting — native ESM doesn’t support jest.mock() hoisting because ES imports are static and resolved before any code runs. Jest’s hoisting only works with CommonJS or Babel-transpiled TypeScript/ESM.
  • Factory function scope — the factory function passed to jest.mock() runs in a sandboxed scope. Variables from the outer scope (including jest.fn() created outside) aren’t accessible without special handling.
  • 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.

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 require

Fix 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 effect

Verify 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 file

Use 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.js

For related Jest issues, see Fix: Jest Fake Timers Not Working and Fix: Jest ESM Error.

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