
When working with tests, especially unit tests, the goal is isolation. We want to ensure that the unit under test behaves correctly, independent of its dependencies. This is where function mocks come into play. They allow us to replace real implementations with controlled, predictable substitutes.
Function mocks serve several purposes beyond just isolation. They let you simulate edge cases that might be hard to reproduce naturally, like network failures or unexpected inputs. They also give you insight into how your code interacts with its dependencies by recording calls and arguments.
Consider a function that fetches user data from a remote API. Testing this function directly against the real API introduces variability and can cause tests to be slow or flaky. By mocking the fetch function, you can return canned responses instantly, ensuring your tests run quickly and deterministically.
Mocks also help in verifying that your code behaves as expected in terms of interaction. For example, you might want to confirm that a logging function was called with a specific message or that a callback was triggered exactly once. These kinds of behavioral assertions are difficult without mocks.
In essence, function mocks are about control and observability. They give you control over external behavior and observability into how your code exercises its dependencies. This combination especially important for writing robust, maintainable tests that provide confidence without the noise of integration complexity.
Ailun Screen Protector for iPad 11th A16 2025 [11 Inch] / 10th Generation 2022 [10.9 Inch], Tempered Glass [Face ID & Apple Pencil Compatible] Ultra Sensitive Case Friendly [2 Pack]
$7.98 (as of June 3, 2026 23:09 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Creating manual mocks for precise control
Manual mocks are the foundation of precise control in testing. Unlike auto-mocking frameworks that generate mocks automatically, manual mocks require you to explicitly define the behavior of each mocked function. This granularity lets you tailor responses and track calls in ways that suit your specific test scenarios.
To create a manual mock, start by defining a function that mimics the signature of the real dependency. Inside, you can implement logic that returns fixed values, throws errors, or records call details. This mock function can then be injected into your unit under test, replacing the real implementation.
Here’s a simple example. Suppose you have a service that depends on a fetchData function to retrieve information. In your test, you can create a manual mock like this:
function fetchDataMock(url) {
fetchDataMock.calls.push(url);
if (url === '/error') {
throw new Error('Network failure');
}
return { data: 'mocked data for ' + url };
}
fetchDataMock.calls = [];
With this mock, you have several advantages. You can simulate network failures by throwing errors for specific URLs. You can return custom data tailored to each input. And by storing call arguments in an array, you gain insight into how your code interacts with this function.
Injecting the mock into your service might look like this:
class DataService {
constructor(fetcher) {
this.fetcher = fetcher;
}
getData(url) {
return this.fetcher(url);
}
}
const service = new DataService(fetchDataMock);
const result = service.getData('/test');
console.log(result); // { data: 'mocked data for /test' }
console.log(fetchDataMock.calls); // ['/test']
This approach ensures your tests remain deterministic and focused. You control exactly what the dependency does, and you can verify how your code calls it. You can extend this pattern to asynchronous functions by returning promises from your mocks.
function fetchDataMockAsync(url) {
fetchDataMockAsync.calls.push(url);
if (url === '/error') {
return Promise.reject(new Error('Network failure'));
}
return Promise.resolve({ data: 'mocked async data for ' + url });
}
fetchDataMockAsync.calls = [];
Using this async mock, your tests can handle success and failure paths without relying on a real network. This manual setup requires a bit more effort but pays off with exact control and clarity over what your tests are doing under the hood.
It’s common to reset the call tracking state before each test to avoid cross-test pollution:
beforeEach(() => {
fetchDataMock.calls.length = 0;
});
Manual mocks also let you implement complex behaviors that might depend on internal state or multiple calls, which can be cumbersome or impossible with automated mocks. For example, you can count how many times a function was called and change its output accordingly:
function fetchDataMockStateful(url) {
fetchDataMockStateful.callCount = (fetchDataMockStateful.callCount || 0) + 1;
if (fetchDataMockStateful.callCount === 1) {
return { data: 'first call data' };
} else {
return { data: 'subsequent call data' };
}
}
This level of control is essential when testing code paths that depend on sequences of interactions rather than isolated calls. Manual mocks can be further enhanced with helper methods to reset state, verify calls, or simulate delays.
While manual mocks demand more upfront coding, they remain invaluable for scenarios where the behavior of dependencies needs to be tightly controlled or when verifying intricate interaction patterns. They form the baseline from which many mocking utilities and frameworks build their abstractions.
Next, we’ll explore how Jest’s built-in mock utilities can simplify many of these concerns by automating mock creation and providing rich APIs for tracking and configuring mocks. However, understanding manual mocks first helps appreciate the flexibility and power those utilities offer, preventing you from becoming a black-box consumer of mocking tools without insight into what’s happening beneath the surface.
Using Jest’s built-in mock utilities
Jest’s built-in mock utilities provide a powerful and expressive way to create mocks without the boilerplate of manual implementations. By using functions like jest.fn() and jest.mock(), you gain immediate access to features such as call tracking, argument inspection, and configurable return values, all with minimal setup.
The simplest way to create a mock function is with jest.fn(). This function generates a mock that can replace any dependency and records information about how it was called. For instance:
const myMock = jest.fn();
myMock('first call', 42);
myMock('second call', 100);
console.log(myMock.mock.calls.length); // 2
console.log(myMock.mock.calls[0]); // ['first call', 42]
console.log(myMock.mock.calls[1]); // ['second call', 100]
Here, myMock.mock.calls is an array where each element is an array of arguments for a single call. This built-in tracking eliminates the need to manually push arguments into arrays as in manual mocks.
You can also configure the behavior of the mock function using methods like mockReturnValue, mockReturnValueOnce, and mockImplementation. For example:
const fetchDataMock = jest.fn()
.mockReturnValue({ data: 'default data' });
fetchDataMock('/url1'); // returns { data: 'default data' }
console.log(fetchDataMock.mock.calls.length); // 1
fetchDataMock.mockReturnValueOnce({ data: 'first call data' })
.mockReturnValueOnce({ data: 'second call data' });
console.log(fetchDataMock('/url2')); // { data: 'first call data' }
console.log(fetchDataMock('/url3')); // { data: 'second call data' }
console.log(fetchDataMock('/url4')); // { data: 'default data' }
Using mockReturnValueOnce is useful for simulating different responses over successive calls, without having to write stateful logic yourself.
For asynchronous functions, you can use mockResolvedValue and mockRejectedValue to simulate promises:
const fetchDataAsyncMock = jest.fn()
.mockResolvedValue({ data: 'async default data' });
fetchDataAsyncMock('/url').then(result => {
console.log(result); // { data: 'async default data' }
});
fetchDataAsyncMock.mockRejectedValueOnce(new Error('Network error'));
fetchDataAsyncMock('/error')
.catch(error => {
console.log(error.message); // 'Network error'
});
Jest also offers jest.mock() for automatic mocking of entire modules. This is particularly useful when you want to replace a dependency module with mocks for all its exported functions. Consider a module api.js that exports fetchData:
// api.js
export function fetchData(url) {
// real implementation making network requests
}
You can mock this module in your test file like so:
jest.mock('./api', () => ({
fetchData: jest.fn().mockResolvedValue({ data: 'mocked module data' }),
}));
import { fetchData } from './api';
test('uses mocked fetchData', async () => {
const result = await fetchData('/test');
expect(result).toEqual({ data: 'mocked module data' });
expect(fetchData).toHaveBeenCalledWith('/test');
});
This approach completely replaces the real fetchData with a mock, eliminating the need to inject dependencies manually. You get the same call tracking and configuration benefits as with jest.fn().
Another useful feature is the ability to reset mocks between tests to prevent state leakage:
beforeEach(() => {
jest.clearAllMocks();
});
This clears call counts and mock implementations, ensuring each test starts with a clean slate.
For complex scenarios, mockImplementation lets you define custom behavior, similar to manual mocks but with automatic call tracking:
const fetchDataMock = jest.fn().mockImplementation(url => {
if (url === '/error') {
throw new Error('Simulated failure');
}
return { data: 'mocked data for ' + url };
});
This combines the flexibility of manual mocks with the convenience of Jest’s tracking features.
Finally, Jest mocks can be inspected and asserted against with built-in matchers, such as toHaveBeenCalledTimes, toHaveBeenCalledWith, and toHaveBeenLastCalledWith. For example:
expect(fetchDataMock).toHaveBeenCalledTimes(2);
expect(fetchDataMock).toHaveBeenCalledWith('/test');
expect(fetchDataMock).toHaveBeenLastCalledWith('/final');
These assertions make it simpler to verify the interaction patterns between your code and its dependencies, improving the expressiveness and readability of your tests.
