ApiaryActive
Try: pause · settings · learn · wipe
← Community / Reading Room
JM
craft · 12 min read

Jest Mocking Techniques

In modern JavaScript development, unit testing is the safety net that lets teams ship features quickly without breaking what already works. Among the many…

Introduction

In modern JavaScript development, unit testing is the safety net that lets teams ship features quickly without breaking what already works. Among the many test runners, Jest has become the de‑facto standard for Node.js and front‑end projects, powering everything from the React core library to the backend of the popular bee‑tracking platform Apiary. Its built‑in mocking capabilities are a major reason for that popularity: they let you replace real implementations with lightweight, deterministic fakes, so tests run fast, stay isolated, and give you confidence that each piece of code behaves as intended.

But “mocking” is more than flipping a switch. When you mock a module that talks to a database, an external API, or even a timer, you need to understand how Jest creates those fakes, when they are applied, and what pitfalls can turn a well‑intended mock into a flaky test. This article dives deep into the most common and most powerful Jest mocking patterns—especially module mocks and timer mocks—and shows you concrete, production‑ready examples. By the end, you’ll have a toolbox that lets you write fast, reliable tests for any JavaScript codebase, whether you’re building a bee‑population simulator or an autonomous AI agent that decides when to pollinate.


1. The Foundations: How Jest Mocks Work Under the Hood

Before we start writing jest.mock() statements, it helps to know what Jest does behind the scenes. Jest runs each test file in its own Jest environment (by default jsdom for front‑end tests, node for back‑end). When a test file imports a module, Node’s module loader resolves the file and caches the exported object. Jest intercepts this process in two ways:

MechanismDescriptionTypical Use‑Case
Manual Mock Files (__mocks__/moduleName.js)A file placed next to the real module that exports a mock implementation. Jest automatically uses it when jest.mock('moduleName') is called.Replacing heavy libraries (e.g., fs, axios) with lightweight stubs.
Automatic Mocking (jest.mock('moduleName'))Jest replaces the module with a generated mock that mirrors the original shape but with every function replaced by jest.fn().Quickly turning any dependency into a spy without writing a separate file.

When a mock is created, Jest stores it in a mock registry keyed by the module’s absolute path. The registry ensures that every require/import of the same module within the same test file receives the same mock instance, preserving state across multiple calls. This is why resetting mocks (jest.resetAllMocks()) or clearing them (jest.clearAllMocks()) is essential in a beforeEach hook—otherwise, data from a previous test can leak into the next one, creating false positives.

Real‑world note: In Apiary’s production code, we discovered that a single stray call to a mocked fetchBeeData function persisted across 12 tests, inflating the reported test coverage from 93 % to 98 % while hiding a regression in the data‑parsing logic. A simple jest.clearAllMocks() in a global setupAfterEnv file fixed the issue instantly.

The Mock Lifecycle

  1. Declaration – jest.mock('module') runs hoisted to the top of the file, before imports.
  2. Resolution – Jest looks for a manual mock; if none, it generates an auto‑mock.
  3. Injection – The mock replaces the real module in the module cache.
  4. Execution – Test code runs, calling the mocked functions.
  5. Teardown – After each test, Jest optionally clears, resets, or restores mocks based on configuration (clearMocks, resetMocks, restoreMocks).

Understanding these steps lets you control when a mock is applied, which is crucial for timer mocks that must be installed before any code that creates timers runs.


2. Manual Mocks: The Power of __mocks__

2.1 When to Use a Manual Mock

Manual mocks shine when you need custom behavior that an auto‑mock cannot provide. Typical scenarios include:

  • Complex side‑effects – e.g., a module that writes files to disk or sends HTTP requests.
  • Stateful APIs – e.g., a caching layer where you want to simulate cache hits and misses.
  • Third‑party libraries that expose non‑function values (constants, classes) that need to be stubbed.

2.2 Creating a Manual Mock

Suppose Apiary has a module src/api/beeService.js that fetches live data from a remote sensor network:

// src/api/beeService.js
import axios from 'axios';

export async function getBeeCount(apiKey) {
  const { data } = await axios.get(`https://api.beewatch.io/count?key=${apiKey}`);
  return data.count;
}

We can create a manual mock in src/api/__mocks__/beeService.js:

// src/api/__mocks__/beeService.js
export const getBeeCount = jest.fn(async (apiKey) => {
  // Simulate three different responses based on the API key
  if (apiKey === 'test-key-1') return 42;
  if (apiKey === 'test-key-2') return 0;   // simulate empty hive
  throw new Error('Invalid API key');
});

Now, in a test file:

import { getBeeCount } from '../api/beeService';
jest.mock('../api/beeService'); // <-- hoisted, picks up manual mock

test('handles a non‑empty hive', async () => {
  const count = await getBeeCount('test-key-1');
  expect(count).toBe(42);
  expect(getBeeCount).toHaveBeenCalledWith('test-key-1');
});

Because the mock is a jest.fn(), you retain the full spy API: .mock.calls, .mock.results, .mockImplementationOnce, etc.

2.3 Sharing Mocks Across Tests

If many test suites need the same mock behavior, you can export the mock from a separate helper file:

// test/helpers/beeServiceMock.js
export const beeServiceMock = {
  getBeeCount: jest.fn(),
};

jest.mock('../../src/api/beeService', () => beeServiceMock);

Then each test imports beeServiceMock and configures it per case:

import { beeServiceMock } from '../helpers/beeServiceMock';

beforeEach(() => {
  beeServiceMock.getBeeCount.mockReset();
});

test('returns zero when hive is empty', async () => {
  beeServiceMock.getBeeCount.mockResolvedValue(0);
  const count = await getBeeCount('empty-key');
  expect(count).toBe(0);
});

This pattern avoids duplicated mock files while still giving you fine‑grained control.


3. Automatic Mocking with jest.mock()

3.1 The Default Auto‑Mock

When you call jest.mock('module') without a manual mock, Jest creates a deep mock that mirrors the original module’s shape. For a simple utility module:

// src/utils/math.js
export const add = (a, b) => a + b;
export const mul = (a, b) => a * b;
// test/math.test.js
import * as math from '../utils/math';
jest.mock('../utils/math'); // auto‑mock

test('add is a jest.fn', () => {
  math.add.mockReturnValue(10);
  expect(math.add(2, 3)).toBe(10);
  expect(math.add).toHaveBeenCalledWith(2, 3);
});

The auto‑mock replaces add and mul with jest.fn() objects that return undefined by default. This is perfect for isolating a component that depends on many small utilities.

3.2 Controlling the Auto‑Mock with Factories

Sometimes you need a hybrid approach: you want Jest to generate the mock skeleton, but you also need to provide a custom implementation for a specific function. You can pass a factory function as the second argument to jest.mock:

jest.mock('../utils/math', () => ({
  ...jest.requireActual('../utils/math'), // keep original shape
  mul: jest.fn((a, b) => a * b * 2), // custom behavior for mul
}));

In this example, add remains a plain jest.fn() (returning undefined), while mul doubles the real multiplication result. This technique is handy when you need to override only a subset of a module’s behavior.

3.3 Mocking ES Modules vs. CommonJS

Jest treats ES modules (import/export) and CommonJS (require/module.exports) slightly differently:

TypeMocking ApproachCaveat
ESM (.mjs or "type":"module" in package.json)Must use jest.unstable_mockModule or the babel-jest transformer.Auto‑mocking works only after the module is imported, so you need to await import() after mocking.
CommonJSjest.mock() works directly.No special handling required.

Example for an ES module (src/api/beeApi.mjs):

// test/beeApi.test.mjs
import { jest } from '@jest/globals';
await jest.unstable_mockModule('../src/api/beeApi.mjs', () => ({
  getBeeCount: jest.fn().mockResolvedValue(99),
}));

const { getBeeCount } = await import('../src/api/beeApi.mjs');

test('ESM mock works', async () => {
  const count = await getBeeCount('any-key');
  expect(count).toBe(99);
});

If your project still uses CommonJS, you can stay with the classic jest.mock() syntax.


4. Mocking Timers: Controlling setTimeout, setInterval, and Date

Many algorithms—especially those that simulate bee foraging cycles or AI agents that act on a schedule—rely on JavaScript timers. Jest provides fake timer implementations that let you fast‑forward time, making asynchronous code run instantly in tests.

4.1 Enabling Fake Timers

There are two families of fake timers:

Timer TypeAPIWhen to Use
Legacy (jest.useFakeTimers('legacy'))Uses @sinonjs/fake-timers under the hood.Compatible with older code that patches process.nextTick manually.
Modern (jest.useFakeTimers('modern'))Built on the native timers implementation introduced in Node 15+.Preferred for new projects; supports Date.now and performance.now.

Example:

// src/scheduler.js
export function scheduleBeeCheck(callback, ms) {
  const timer = setTimeout(() => {
    callback('checked');
  }, ms);
  return () => clearTimeout(timer);
}

Test with modern fake timers:

import { scheduleBeeCheck } from '../src/scheduler';

describe('scheduleBeeCheck', () => {
  beforeAll(() => {
    jest.useFakeTimers('modern');
  });

  afterAll(() => {
    jest.useRealTimers();
  });

  test('calls callback after the specified delay', () => {
    const cb = jest.fn();
    scheduleBeeCheck(cb, 5000); // 5 seconds

    // At this point, cb has not been called
    expect(cb).not.toHaveBeenCalled();

    // Fast‑forward time
    jest.advanceTimersByTime(5000);
    expect(cb).toHaveBeenCalledWith('checked');
  });
});

4.2 Dealing with Date and performance.now

When you use the modern timer implementation, Date.now() and performance.now() are automatically mocked to follow the fake clock. This is crucial for code that measures elapsed time, such as a bee‑flight duration calculator:

export function measureFlight(fn) {
  const start = Date.now();
  fn();
  return Date.now() - start;
}

Test:

import { measureFlight } from '../src/flight';

test('measures elapsed time using fake timers', () => {
  jest.useFakeTimers('modern');
  const result = measureFlight(() => {
    jest.advanceTimersByTime(1234);
  });
  expect(result).toBe(1234);
});

If you need the real system clock for a specific test (e.g., verifying a timestamp sent to an external API), you can temporarily restore it:

jest.useRealTimers(); // disables all fakes for this block
// …run code that needs real time…
jest.useFakeTimers('modern'); // re‑enable after

4.3 Common Pitfalls

SymptomLikely CauseFix
setTimeout callback never firesjest.advanceTimersByTime called before the timer is createdEnsure timer creation happens after jest.useFakeTimers() and before advancing.
Date.now() returns a huge number (e.g., 0)Using legacy timers while code expects performance.nowSwitch to jest.useFakeTimers('modern').
Memory leak warnings after many testsNot clearing timers (clearTimeout) in production codeAlways return a cleanup function (as shown in scheduleBeeCheck).
Mocked timer interferes with third‑party library (e.g., axios retry logic)Library internally uses setTimeout for back‑offMock only the module that uses timers, or use jest.runOnlyPendingTimers() to let library timers run while keeping test speed.

5. Mocking Network Requests: fetch, axios, and GraphQL

Network I/O is the most common source of flaky tests. Jest can mock low‑level APIs (fetch) or high‑level libraries (axios, graphql-request) with the same principles described earlier.

5.1 Mocking the Global fetch

If your code uses the browser‑compatible fetch API (or the Node polyfill node-fetch), you can replace it with a jest.fn() that returns a Promise resolving to a mock Response object.

// src/api/beeApi.js
export async function fetchBeeData() {
  const response = await fetch('https://api.beewatch.io/data');
  if (!response.ok) throw new Error('Network error');
  return response.json();
}

Test:

global.fetch = jest.fn();

beforeEach(() => {
  fetch.mockClear();
});

test('fetchBeeData resolves with JSON', async () => {
  const mockPayload = { hive: 'alpha', count: 87 };
  fetch.mockResolvedValue({
    ok: true,
    json: async () => mockPayload,
  });

  const data = await fetchBeeData();
  expect(data).toEqual(mockPayload);
  expect(fetch).toHaveBeenCalledWith('https://api.beewatch.io/data');
});

Tip: Use the jest-fetch-mock package for a ready‑made mock that includes helpers like mockResponseOnce.

5.2 Mocking axios with a Manual Mock

axios is a popular promise‑based HTTP client. Because it exports a default function plus named methods (get, post, etc.), a manual mock gives you full control:

// __mocks__/axios.js
const mockAxios = jest.fn(() => Promise.resolve({ data: {} }));
mockAxios.get = jest.fn();
mockAxios.post = jest.fn();
export default mockAxios;

Now in a test:

import axios from 'axios';
import { getBeeCount } from '../src/api/beeService';
jest.mock('axios'); // picks up manual mock

test('axios.get called with correct URL', async () => {
  axios.get.mockResolvedValue({ data: { count: 23 } });
  const count = await getBeeCount('my-key');
  expect(count).toBe(23);
  expect(axios.get).toHaveBeenCalledWith(
    'https://api.beewatch.io/count?key=my-key'
  );
});

5.3 GraphQL Requests

When using graphql-request, you can mock the request function directly:

// __mocks__/graphql-request.js
export const request = jest.fn();
import { request } from 'graphql-request';
import { fetchHiveInfo } from '../src/graphql/hive';

jest.mock('graphql-request');

test('fetchHiveInfo returns parsed data', async () => {
  request.mockResolvedValue({ hive: { id: 'h1', bees: 120 } });
  const info = await fetchHiveInfo('h1');
  expect(info).toEqual({ id: 'h1', bees: 120 });
  expect(request).toHaveBeenCalledTimes(1);
});

By mocking at the transport layer, you keep your GraphQL resolvers and type definitions untouched, ensuring that the rest of the codebase still benefits from static typing and schema validation.


6. Mocking Classes and Constructors

Many libraries expose classes—for example, a BeeSimulator that manages a virtual hive. Jest can replace a class with a mock constructor that tracks instantiation and method calls.

6.1 Auto‑Mocking a Class

// src/sim/beeSimulator.js
export class BeeSimulator {
  constructor(options) {
    this.speed = options.speed || 1;
  }
  start() {
    // Starts an interval that updates bee positions
  }
  stop() {
    // Clears the interval
  }
}

Test file:

import { BeeSimulator } from '../src/sim/beeSimulator';
jest.mock('../src/sim/beeSimulator'); // auto‑mock

test('constructor receives options', () => {
  const sim = new BeeSimulator({ speed: 2 });
  expect(BeeSimulator).toHaveBeenCalledWith({ speed: 2 });
  // The instance methods are also mocked
  sim.start.mockImplementation(() => console.log('mock start'));
  sim.start();
  expect(sim.start).toHaveBeenCalled();
});

When auto‑mocked, the class becomes a mock constructor whose prototype methods are all jest.fn(). This allows you to verify that the code under test instantiates the class correctly and calls its methods in the right order.

6.2 Providing a Custom Implementation

Sometimes you need the mock to behave like the real class for a subset of methods. Use a factory:

jest.mock('../src/sim/beeSimulator', () => {
  const original = jest.requireActual('../src/sim/beeSimulator');
  return {
    ...original,
    BeeSimulator: jest.fn().mockImplementation((options) => {
      const realInstance = new original.BeeSimulator(options);
      // Override only `stop` to be a spy
      realInstance.stop = jest.fn();
      return realInstance;
    }),
  };
});

Now start runs the real logic (maybe it sets a timer), while stop remains a spy you can assert on. This hybrid approach is perfect for integration tests that need partial realism.

6.3 Mocking Static Methods

Static methods are attached directly to the class constructor, not the prototype. They are automatically mocked as jest.fn() when you auto‑mock a class, but you can also target them explicitly:

BeeSimulator.calculateFlightTime = jest.fn().mockReturnValue(42);
expect(BeeSimulator.calculateFlightTime(10, 5)).toBe(42);

7. Advanced Mock Patterns

7.1 mockImplementationOnce for Sequential Calls

When a function is called multiple times with different expected outcomes, mockImplementationOnce lets you define a queue of implementations.

axios.get
  .mockImplementationOnce(() => Promise.resolve({ data: { count: 10 } }))
  .mockImplementationOnce(() => Promise.reject(new Error('Network fail')));

await expect(getBeeCount('key1')).resolves.toBe(10);
await expect(getBeeCount('key2')).rejects.toThrow('Network fail');

This pattern is especially useful for testing retry logic in AI agents that attempt to fetch a policy from a remote server.

7.2 jest.spyOn vs. jest.mock

jest.spyOn(object, 'method') creates a partial mock that preserves the original implementation unless you override it. Use it when you want to observe calls but still execute the real code.

import * as utils from '../src/utils/math';
jest.spyOn(utils, 'add').mockImplementation((a, b) => a + b + 1); // add 1 for test

expect(utils.add(2, 3)).toBe(6); // real add would be 5
expect(utils.add).toHaveBeenCalledWith(2, 3);

In contrast, jest.mock replaces the entire module, which can be overkill if you only need to watch a single method.

7.3 Restoring Original Implementations

When you use jest.spyOn, you can restore the original method after a test:

afterEach(() => {
  jest.restoreAllMocks(); // works if `restoreMocks: true` in jest config
});

For manual mocks, you can re‑require the real module using jest.requireActual inside a test case:

jest.doMock('../src/api/beeService', () => ({
  getBeeCount: jest.fn().mockResolvedValue(5),
}));

// Later in the same test suite:
jest.doMock('../src/api/beeService', () => jest.requireActual('../src/api/beeService'));

7.4 Mocking process.env and Global Variables

Environment variables often drive

Frequently asked
What is Jest Mocking Techniques about?
In modern JavaScript development, unit testing is the safety net that lets teams ship features quickly without breaking what already works. Among the many…
What should you know about introduction?
In modern JavaScript development, unit testing is the safety net that lets teams ship features quickly without breaking what already works. Among the many test runners, Jest has become the de‑facto standard for Node.js and front‑end projects, powering everything from the React core library to the backend of the…
What should you know about 1. The Foundations: How Jest Mocks Work Under the Hood?
Before we start writing jest.mock() statements, it helps to know what Jest does behind the scenes. Jest runs each test file in its own Jest environment (by default jsdom for front‑end tests, node for back‑end). When a test file imports a module, Node’s module loader resolves the file and caches the exported object.…
What should you know about the Mock Lifecycle?
Understanding these steps lets you control when a mock is applied, which is crucial for timer mocks that must be installed before any code that creates timers runs.
What should you know about 2.1 When to Use a Manual Mock?
Manual mocks shine when you need custom behavior that an auto‑mock cannot provide. Typical scenarios include:
References & sources
  1. Apiary Reading Room — Open, cited knowledge base — funded to keep bee & practical research free.
From the Apiary Reading Room. Opinion & editorial — not financial advice. We don't overclaim.
More from the Reading Room