Skip to main content

Mocking functions

Functions that save, delete or update data (via an API) are often fundamental features of an application. For example, updating one's Instagram profile, logging your workout on a sports app or adding an entry in your agenda. Therefore, testing integrity of such functions is of high significance when ensuring an application works as intended. However, adding or deleting data while testing an application is suboptimal: it stresses the database, introduces fictitious data, slows down the test and is also subject to integrity of the API being called.

Testing an API call with a mock function

Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls) and allowing you to check the return values.

Below, see an example in which we mock an API call that normally returns an array of time entries.

import { fetchTimeEntries } from "../time-entries-api";

const mockedTimeEntries = [
{
activity: "developmentwork",
client: "Port of Rotterdam",
startTime: "2021-10-08T07:01:00.000Z",
endTime: "2021-10-08T15:00:00.000Z",
id: 0,
},
];

test("if time entries are fetched from the server", async () => {
const mockFetchResponse = Promise.resolve({
json: () => Promise.resolve(mockedTimeEntries),
});

global.fetch = jest.fn().mockImplementationOnce(() => mockFetchResponse);

const response = getTimeEntries();

expect(response).toEqual(mockFetchResponse);
expect(global.fetch).toHaveBeenCalledTimes(1);
waitFor(() => expect(global.fetch).toHaveBeenCalledWith(
`${process.env.NEXT_PUBLIC_DB_HOST}/time-entries?_sort=startTime&_order=asc`
));
});

In the above example, we're actually replacing the original global.fetch function with our own mocked function. In other words: normally, calling the fetchTimeEntries function would apply the original global.fetch function. By temporarily replacing the global.fetch with our own "mock-fetch" function, we verify if calling our fetchTimeEntries function, (1) actually calls global.fetch, and (2) whether it is called with the correct URL.

An important thing to note is that the mocked function must be compatible with the real thing. fetch returns a resolved Promise with a JSON method, which on its turn also returns a Promise with the actual JSON data. To properly mock this, we need to do the same thing inside our mock fetch.

Finally, we chose to only mock it exactly one time using mockImplementationOnce. This way the function reverts to its original state after first use.

The waitFor() function is used to wait for the service to finish before continuing the test. Otherwise the test will continue to run before the service is retrieved and this will result in an error.

Testing an unhappy flow

To make sure our test covers more than just the desired scenario, we also need to walk through various unhappy flows. In the next example we'll test if the logic returns an error when the endpoint returns a 404 status. This number is referred to as an HTTP status code. These are server status codes which tells us what happened to our request. 404 is code for 'The server can not find the requested resource'. Read more about these codes on MDN.

import { fetchTimeEntries } from "../time-entries-api";
import { NotFoundError } from "../errors";

test("if a notFoundError instance is returned after getting a 404", async () => {
const mockFetchResponse = Promise.resolve({
status: 404,
});
global.fetch = jest.fn().mockImplementationOnce(() => mockFetchResponse);

const response = await getTimeEntries();

expect(response).toBeInstanceOf(NotFoundError);
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
`${process.env.NEXT_PUBLIC_DB_HOST}/time-entries?_sort=startTime&_order=asc`
);
});

Notice how we're using Promise.resolve for our mock function. When a promise fails, a Promise.reject is used. While fetching, the promise is only rejected on network failure or if anything prevented the request from completing. This means the Promise returned from fetch() won’t reject on an HTTP error status even if the status response would be 404 or 500.

Finally, we didn't need to add a json to our mockFetchResponse. We replaced it with the status: 404. Look at the mockFetchResponse function. The 404 status is checked before the response.json() is called in the getTimeEntries function. When the error is seen the code is stopped dead in its tracks and the error is returned to our test.

Further reading