Best Practices for Mocking Lodash in Jest Tests

Mocking the Lodash JavaScript library in Jest is often necessary when testing code that relies on time-sensitive functions like debounce and throttle, or non-deterministic helpers like random and uniqueId. This guide covers the best practices for isolating Lodash methods in your test suites, demonstrating how to selectively override functions, preserve un-mocked utilities using jest.requireActual, handle individual path imports, and manage timer-dependent behavior effectively.

1. Avoid Mocking Pure Utility Functions

Do not mock pure Lodash functions such as get, map, cloneDeep, or isEqual. These functions are fast, deterministic, and self-contained. Mocking them adds unnecessary test maintenance, slows down test suites, and can obscure bugs in your implementation. Only mock Lodash utilities when they:

2. Preserve Unmocked Functions with jest.requireActual

When you must mock the root lodash package, never replace the entire module with an empty mock object. Doing so breaks any other code in your project relying on standard Lodash utilities. Instead, use jest.requireActual to preserve the real implementations and selectively override only the methods you need.

jest.mock('lodash', () => {
  const actualLodash = jest.requireActual('lodash');
  return {
    ...actualLodash,
    debounce: jest.fn((fn) => fn), // Execute immediately instead of debouncing
    uniqueId: jest.fn(() => 'mock-id-123'),
  };
});

3. Prefer jest.spyOn for Targeted, Scoped Mocks

If only a single test or test suite requires a mocked Lodash behavior, use jest.spyOn() instead of a global jest.mock(). This scopes the mock to specific assertions and allows easy restoration.

import _ from 'lodash';

describe('Payment Service', () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('generates a predictable unique ID', () => {
    jest.spyOn(_, 'uniqueId').mockReturnValue('static-id-456');

    // Run code under test
    expect(_.uniqueId()).toBe('static-id-456');
  });
});

4. Handle Per-Method Imports Correctly

In modern projects, developers often import specific Lodash methods directly to reduce bundle size (e.g., import debounce from 'lodash/debounce'). Mocking the root lodash module will not affect these submodule imports. You must mock the direct path matching the import statement:

// When the source code uses: import debounce from 'lodash/debounce';

jest.mock('lodash/debounce', () => {
  // Return the mock implementation directly for default exports
  return jest.fn((fn) => {
    fn.cancel = jest.fn();
    fn.flush = jest.fn();
    return fn;
  });
});

If your codebase uses named imports from lodash-es, adjust the mock target accordingly:

jest.mock('lodash-es', () => ({
  ...jest.requireActual('lodash-es'),
  throttle: jest.fn((fn) => fn),
}));

5. Implement Passthrough Functions for debounce and throttle

Functions wrapped in debounce or throttle delay execution, which complicates assertions. Rather than advancing fake timers, the most reliable practice for unit tests is replacing them with an immediate passthrough:

const makePassthrough = (fn) => {
  const wrapper = (...args) => fn(...args);
  wrapper.cancel = jest.fn();
  wrapper.flush = jest.fn();
  return wrapper;
};

jest.mock('lodash/debounce', () => jest.fn(makePassthrough));

This pattern ensures the underlying callback runs synchronously during the test while maintaining compatibility with helper methods like .cancel() and .flush().

6. Clear and Restore Mocks Between Tests

When using spies or overriding implementations dynamically, always reset or restore mocks to avoid cross-test pollution. Configure Jest's clearMocks and restoreMocks options in jest.config.js:

module.exports = {
  clearMocks: true,
  restoreMocks: true,
};

If configuring manually inside test files, call jest.restoreAllMocks() in an afterEach block to return Lodash functions to their original states.