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
- For another example of mocking a fetch function, visit: https://benjaminjohnson.me/blog/mocking-fetch
- For more general information on mocking functions, visit: https://jestjs.io/docs/mock-functions
- using fetch
- Useful examples