Jest’s mocking capabilities let you swap out parts of your Node.js application for controlled, predictable substitutes, enabling isolated unit testing.
Let’s say you have a module userService.js that fetches user data from an external API and a function getUserName(userId) in userService.js that retrieves this data and returns the user’s name.
// userService.js
const axios = require('axios');
async function getUserData(userId) {
const response = await axios.get(`https://api.example.com/users/${userId}`);
return response.data;
}
async function getUserName(userId) {
try {
const userData = await getUserData(userId);
return userData.name;
} catch (error) {
console.error('Error fetching user data:', error);
return 'Unknown User';
}
}
module.exports = { getUserName, getUserData };
Now, you want to test getUserName without actually hitting the api.example.com endpoint. This is where Jest’s jest.mock comes in.
Imagine you’re testing the getUserName function. You want to ensure it handles both successful API responses and errors gracefully.
Here’s how you’d set up your test file, userService.test.js:
// userService.test.js
const userService = require('./userService');
// Mock the entire axios module
jest.mock('axios');
describe('getUserName', () => {
// Clear all mocks before each test
beforeEach(() => {
axios.get.mockClear();
});
test('should return the user name on successful API call', async () => {
// Mock a successful response for axios.get
axios.get.mockResolvedValue({
data: {
id: 1,
name: 'Alice'
}
});
const userName = await userService.getUserName(1);
expect(userName).toBe('Alice');
// Verify that axios.get was called with the correct URL
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
});
test('should return "Unknown User" if API call fails', async () => {
// Mock a rejected promise for axios.get to simulate an error
axios.get.mockRejectedValue(new Error('API Error'));
const userName = await userService.getUserName(2);
expect(userName).toBe('Unknown User');
// Verify that axios.get was called even on error
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/2');
});
});
In this example, jest.mock('axios') tells Jest to replace the actual axios module with a mock version. Then, within each test, axios.get.mockResolvedValue and axios.get.mockRejectedValue are used to control the behavior of the axios.get function. This allows us to simulate different network conditions without making real HTTP requests.
You can also mock specific functions within a module. Suppose userService.js had another helper function formatUserData that you wanted to mock.
// userService.js (modified)
const axios = require('axios');
async function getUserData(userId) {
const response = await axios.get(`https://api.example.com/users/${userId}`);
return response.data;
}
function formatUserData(userData) {
// Complex formatting logic
return `Formatted: ${userData.name} (ID: ${userData.id})`;
}
async function getUserName(userId) {
try {
const userData = await getUserData(userId);
return formatUserData(userData); // Using the helper
} catch (error) {
console.error('Error fetching user data:', error);
return 'Unknown User';
}
}
module.exports = { getUserName, getUserData, formatUserData };
And the test would look like this:
// userService.test.js (modified)
const userService = require('./userService');
// Mock the entire userService module to control its internal functions
jest.mock('./userService');
describe('getUserName', () => {
test('should return formatted user name using mocked helper', async () => {
// Mock the internal formatUserData function
userService.formatUserData.mockReturnValue('Mocked Formatted Name');
// We don't need to mock axios here if we're mocking the whole userService
// and only testing the public interface of getUserName.
// If getUserName *also* called external services that we *didn't* want to mock
// via the top-level mock, we'd need to be more granular.
const userName = await userService.getUserName(3);
expect(userName).toBe('Mocked Formatted Name');
// Verify that formatUserData was called with some data (though we didn't mock getUserData)
// This requires us to know what getUserData *would* return for the mock to work.
// A more robust test would mock both.
expect(userService.formatUserData).toHaveBeenCalled();
});
});
In the second example, jest.mock('./userService') replaces the entire userService module. This means userService.getUserName is now a mock function. We then specifically mock userService.formatUserData to return a predictable value. When userService.getUserName(3) is called, it will execute its mocked implementation, which in this case, we haven’t explicitly defined, so it will likely just return undefined unless we also mock getUserData.
A common pattern is to mock specific functions within a module when you only need to isolate a single function and its dependencies.
// userService.test.js (more refined)
const axios = require('axios');
const userService = require('./userService'); // Original module
// Mock axios, but keep the original userService implementation for getUserName
jest.mock('axios');
describe('getUserName (with specific function mocking)', () => {
beforeEach(() => {
axios.get.mockClear();
// Clear any custom mock implementations for userService functions
jest.restoreAllMocks(); // This is important if you've used jest.spyOn or other mocks
});
test('should return formatted user name using actual getUserName but mocked dependencies', async () => {
// Mocking getUserData specifically
const mockUserData = { id: 4, name: 'Bob' };
const getUserDataSpy = jest.spyOn(userService, 'getUserData');
getUserDataSpy.mockResolvedValue(mockUserData);
// Mocking formatUserData specifically
const formatUserDataSpy = jest.spyOn(userService, 'formatUserData');
formatUserDataSpy.mockReturnValue('Bob (ID: 4) - Processed');
const userName = await userService.getUserName(4);
expect(userName).toBe('Bob (ID: 4) - Processed');
expect(getUserDataSpy).toHaveBeenCalledWith(4);
expect(formatUserDataSpy).toHaveBeenCalledWith(mockUserData);
});
test('should handle errors from getUserData', async () => {
const getUserDataSpy = jest.spyOn(userService, 'getUserData');
getUserDataSpy.mockRejectedValue(new Error('DB Error'));
// Ensure formatUserData is not called in case of error
const formatUserDataSpy = jest.spyOn(userService, 'formatUserData');
const userName = await userService.getUserName(5);
expect(userName).toBe('Unknown User');
expect(getUserDataSpy).toHaveBeenCalledWith(5);
expect(formatUserDataSpy).not.toHaveBeenCalled();
});
});
Here, jest.spyOn(userService, 'getUserData') creates a spy that wraps the original getUserData function. This allows us to track calls to it and also to provide a mock implementation (mockResolvedValue or mockRejectedValue) without replacing the entire module. jest.restoreAllMocks() in beforeEach is crucial to ensure that mocks from previous tests don’t leak into the current one.
The power of Jest’s mocking lies in its flexibility. You can mock entire modules, individual functions within modules, or even class methods. This allows you to isolate the code you’re testing, making your tests faster, more reliable, and easier to understand.
When you mock a module using jest.mock('module-name'), Jest replaces all exports of that module with mock functions. You can then control the behavior of these mock functions using methods like .mockResolvedValue(), .mockRejectedValue(), .mockReturnValue(), and .mockImplementation(). This enables you to simulate successful operations, errors, or custom logic for dependencies that your code relies on.
The trickiest part is often managing the scope of your mocks and ensuring they are correctly reset between tests. Using beforeEach with mockClear() or jest.restoreAllMocks() is essential for preventing test pollution.
If you find yourself needing to mock a dependency that’s deeply nested or conditionally required, you might explore jest.requireActual to get the real module and then selectively mock parts of it, or use jest.spyOn for more fine-grained control over specific functions within an already required module.