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:
- Involve timing (e.g.,
debounce,throttle,delay). - Involve non-deterministic outputs (e.g.,
random,uniqueId,now). - Cause side effects or performance bottlenecks in massive datasets during testing.
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.