Jest’s __mocks__ directory is a powerful tool for isolating your tests and making them faster, but it can be a bit of a black box if you don’t know how it works.
Let’s say you have a module, api.js, that makes actual HTTP requests. You don’t want your unit tests to hit a real server, so you’ll mock it.
// api.js
export const getUser = async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
};
To mock this, you’d create a __mocks__ directory next to your api.js file (or at the root of your src or test directory, depending on your setup) and place a file named api.js inside it.
// __mocks__/api.js
export const getUser = jest.fn(async (userId) => {
console.log(`Mocked getUser called with ${userId}`);
// Simulate different responses for different users
if (userId === '123') {
return { id: '123', name: 'Alice' };
}
if (userId === '456') {
return { id: '456', name: 'Bob' };
}
throw new Error('User not found');
});
Now, in your test file, you can tell Jest to use this mock:
// __tests__/user.test.js
import { getUser } from '../api'; // Jest will automatically pick up the mock
describe('getUser', () => {
it('should return user data for a valid ID', async () => {
const user = await getUser('123');
expect(user).toEqual({ id: '123', name: 'Alice' });
expect(getUser).toHaveBeenCalledWith('123');
expect(getUser).toHaveBeenCalledTimes(1);
});
it('should throw an error for an invalid ID', async () => {
await expect(getUser('789')).rejects.toThrow('User not found');
expect(getUser).toHaveBeenCalledWith('789');
});
});
When you run npm test, Jest sees the import { getUser } from '../api'; and, because api.js is in the __mocks__ directory adjacent to the original api.js, it automatically substitutes the real api.js with the mocked version. The jest.fn() creates a mock function that tracks calls, arguments, and return values, allowing you to assert on its behavior.
The most surprising true thing about Jest’s __mocks__ directory is that it’s not just for modules directly imported. It also automatically mocks modules that are dependencies of your explicitly mocked modules, if those dependencies are also found within a node_modules path relative to the mock. This means you can mock a complex service layer, and Jest will happily mock its underlying HTTP client (like axios or node-fetch) if it’s also in a __mocks__ directory.
The internal mechanism is quite clever: when Jest encounters an import statement, it first checks if a corresponding mock exists in a __mocks__ directory relative to the module being imported. If it finds one, it uses that mock instead of the actual module. This lookup happens recursively for all imported modules within the mocked module. This allows you to create isolated, predictable environments for your tests without needing to manually mock every single dependency.
The next concept you’ll likely encounter is how to mock modules that are installed via npm, using jest.mock('module-name', factory).