Mocking Modules and Dependencies
When testing JavaScript code, you often need to isolate the unit under test from its dependencies. Database calls, API requests, file system operations, and third-party libraries are all examples of dependencies that can make tests slow, flaky, or impossible to run in isolation. This is where mocking comes in.
In this tutorial, you’ll learn how to mock modules, functions, and entire dependencies using Jest and Vitest. We’ll cover function mocks, module mocking, spies, and timer faking—everything you need to write fast, reliable, isolated tests.
Why Mocking Matters
Consider this function that fetches user data from an API:
// userService.js
const axios = require('axios');
async function getUserById(id) {
const response = await axios.get(`/api/users/${id}`);
return response.data;
}
module.exports = { getUserById };
To test this function without mocking, you’d need a running API server. This makes your tests:
- Slow — network requests take time
- Flaky — network issues cause random failures
- Hard to maintain — tests depend on external state
Mocking solves all three problems by replacing real dependencies with controlled test doubles.
Mocking Functions
The most basic form of mocking is replacing a single function with a mock implementation.
Creating Mock Functions
Both Jest and Vitest provide a way to create mock functions:
// Jest
const mockFn = jest.fn();
// Vitest
const mockFn = vi.fn();
You can provide a default return value:
const mockFn = jest.fn(() => 'default value');
mockFn(); // 'default value'
Or use mockReturnValue/mockReturnValueOnce for more control:
const mockFn = jest.fn()
.mockReturnValue('first')
.mockReturnValue('second')
.mockReturnValue('default');
mockFn(); // 'first'
mockFn(); // 'second'
mockFn(); // 'default'
mockFn(); // 'default'
Mocking Implementation
For functions that need to perform calculations or transformations:
const calculateDiscount = jest.fn((price, rate) => {
return price * (rate / 100);
});
calculateDiscount(100, 20); // 20
calculateDiscount; // Mock function
Mocking Modules
When your code imports a module, you can mock the entire module. This is powerful for replacing third-party libraries like axios, lodash, or any npm package.
Using jest.mock()
// api.js
const axios = require('axios');
module.exports = {
getUser: async (id) => {
const response = await axios.get(`/users/${id}`);
return response.data;
}
};
// api.test.js
const api = require('./api');
const axios = require('axios');
// Mock the entire axios module
jest.mock('axios');
const mockedAxios = axios;
test('fetches user successfully', async () => {
// Configure the mock
mockedAxios.get.mockResolvedValue({
data: { id: 1, name: 'Alice', email: 'alice@example.com' }
});
const user = await api.getUser(1);
expect(user).toEqual({
id: 1,
name: 'Alice',
email: 'alice@example.com'
});
expect(mockedAxios.get).toHaveBeenCalledWith('/users/1');
});
The jest.mock() call is hoisted to the top of the file, so it runs before any imports. This means you can mock modules that are imported elsewhere in your test file.
Mocking with doMock (ES Modules)
If you’re using ES modules with import, use jest.doMock() instead:
// ES module version
jest.doMock('./api', () => ({
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}));
import { getUser } from './api';
Partial Module Mocking
Sometimes you only want to mock part of a module while keeping the rest real:
jest.mock('lodash', () => ({
...jest.requireActual('lodash'),
debounce: vi.fn((fn) => fn) // Replace only debounce
}));
Using Spies
Spies let you wrap existing functions while still allowing the original implementation to run. This is useful when you want to verify a function was called without replacing its behavior.
jest.spyOn() / vi.spyOn()
const math = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
test('spies on object methods', () => {
const spy = jest.spyOn(math, 'add');
math.add(2, 3);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(2, 3);
expect(math.add(10, 5)).toBe(15); // Original still works
spy.mockRestore(); // Restore original function
});
Spying on Module Methods
You can also spy on methods of modules you’ve imported:
const api = require('./api');
const axios = require('axios');
jest.mock('axios');
test('spies on axios get', () => {
const spy = jest.spyOn(axios, 'get');
axios.get.mockResolvedValue({ data: { name: 'Bob' } });
api.getUser(1);
expect(spy).toHaveBeenCalledWith('/users/1');
});
Mocking Timers
Code that uses setTimeout, setInterval, or setImmediate can be difficult to test. Jest and Vitest provide fake timers to control time in your tests.
useFakeTimers() / useFakeTimers()
// delayed.js
function notifyUser(callback) {
setTimeout(() => {
callback('User notified');
}, 5000);
}
// delayed.test.js
function notifyUser(callback) {
setTimeout(() => {
callback('User notified');
}, 5000);
}
test('notifies user after delay', () => {
jest.useFakeTimers();
const callback = jest.fn();
notifyUser(callback);
// Timer hasn't fired yet
expect(callback).not.toHaveBeenCalled();
// Fast-forward time
jest.advanceTimersByTime(5000);
// Now callback fired
expect(callback).toHaveBeenCalledWith('User notified');
jest.useRealTimers();
});
Timer Methods
| Method | Description |
|---|---|
jest.runAllTimers() | Run all pending timers |
jest.advanceTimersByTime(ms) | Move time forward by ms |
jest.runOnlyPendingTimers() | Run only pending timers |
Modern Timer API (Vitest)
Vitest offers a cleaner timer API:
import { vi, fn } from 'vitest';
test('timers in Vitest', async () => {
vi.useFakeTimers();
const callback = vi.fn();
notifyUser(callback);
await vi.runTimersToTime(5000);
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
});
Mocking Node.js Modules
Node.js has built-in modules like fs, path, and crypto that you often need to mock.
Mocking fs
jest.mock('fs');
const fs = require('fs');
test('reads file content', () => {
fs.readFileSync.mockReturnValue('file contents');
const content = fs.readFileSync('test.txt', 'utf8');
expect(content).toBe('file contents');
expect(fs.readFileSync).toHaveBeenCalledWith('test.txt', 'utf8');
});
Mocking Environment Variables
test('uses environment variable', () => {
const originalEnv = process.env;
process.env.API_KEY = 'test-key';
// Your code here that uses process.env.API_KEY
process.env = originalEnv; // Restore
});
Or use jest.spyOn for cleaner restoration:
test('spies on process.env', () => {
const spy = jest.spyOn(process.env, 'API_KEY', 'get');
spy.mockReturnValue('test-key');
// Your test code
spy.mockRestore();
});
Best Practices
-
Mock at the appropriate level — Mock the closest dependency to your code, not everything.
-
Keep mocks simple — Complex mocks are hard to maintain and can hide real problems.
-
Verify mock interactions — Use matchers like
toHaveBeenCalledWithto ensure your code calls dependencies correctly. -
Clean up after tests — Call
mockRestore()or usejest.resetModules()when needed. -
Don’t mock everything — Sometimes it’s better to use the real implementation if it’s fast and deterministic.
-
Use descriptive mock data — Mock return values should resemble real data structure.
Summary
You’ve learned the essential techniques for mocking in Jest and Vitest:
- Creating mock functions with
jest.fn()/vi.fn() - Mocking entire modules with
jest.mock() - Using spies to wrap existing functions with
jest.spyOn()/vi.spyOn() - Controlling time with fake timers
- Mocking Node.js built-in modules like
fs - Best practices for effective mocking
These techniques will help you write tests that are fast, reliable, and isolated from external dependencies. In the next tutorial, we’ll explore Testing DOM Code with jsdom to learn how to test browser-specific code.