Updated August 7, 2019
Mocking Fetch API Calls When Using Jest
In this lesson we're going to make a few assumptions
- You're using Jest as your test runner
- You're familiar with the fetch API.
Background Info
We're building an app that makes requests against the https://jsonplaceholder.typicode.com
API but we don't want to actually make requests to that API every time we run our tests. That means we need to mock the fetch
request and substitute a response.
A few things about the API:
- If fetching a list of data (
/posts
) the response will be an array - If fetching a single item (
/posts/1
) the response will be an object with data - If making an invalid request we'll get back an empty object
Configuring the Testing Framework
To install jest run yarn add --dev jest
(if you're using Expo you could alternatively use jest-expo
).
Then, in your package.json
, you'll want to configure jest by adding the following.
package.json
{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "react-native" // alternatively use jest-expo if using expo
}
}
You can then run yarn test
to run tests. It won't do anything because you don't have any tests, yet.
Next, we need to setup our mocking for fetch
. First yarn add --dev jest-fetch-mock
.
Then in package.json
:
package.json
{
"jest": {
"preset": "react-native", // alternatively use jest-expo if using expo
"automock": false,
"setupFiles": ["./setupJest.js"]
}
}
We then need to create a setupJest.js
file in the root of our project.
setupJest.js
global.fetch = require('jest-fetch-mock');
In it we just directly override the global.fetch
function, which is what our app leverages to make remote requests.
Starter Code
With Jest already installed, let's start an API function:
App/util/api.js
export const api = (path, options = {}) => {
return fetch(
`https://jsonplaceholder.typicode.com${path}`,
options
).then((res) => res.json());
};
This function will all a screen to simply call api("/posts")
and it will then make the full request and parse the response into a JSON object.
Requirements
The function above isn't quite done. It should:
- Return the result if it's an array
- Return the result if it's a non-empty object
- Throw an error if the result is an empty object
We can use that information to go ahead and right our tests.
Writing the Tests
Following Jest conventions, we'll create a __tests__/
directory in util/
and put our tests there.
App/util/__tests__/api.test.js
import { api } from '../api';
beforeEach(() => {
fetch.resetMocks();
});
test('returns result if array', () => {
fetch.mockResponseOnce(JSON.stringify([{ id: 1 }]));
});
I add a beforeEach
block which will run before each test in the file is run. In it we reset the fetch mock so that previous tests don't interfere with the test that's currently being run.
Then within the test we actually tell fetch what we want it to return - a stringified array.
App/util/__tests__/api.test.js
// ...
test('returns result if array', () => {
fetch.mockResponseOnce(JSON.stringify([{ id: 1 }]));
const onResponse = jest.fn();
const onError = jest.fn();
return api('/posts')
.then(onResponse)
.catch(onError)
.finally(() => {
expect(onResponse).toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled();
expect(onResponse.mock.calls[0][0][0]).toEqual({ id: 1 });
});
});
The actual test will make a request and, leveraging our functions promises and the use of jest mock functions, we can check that the right functions have been called in this test.
Finally we actually check the result of the test. Since we're using mock functions we can check how many times it has been called and what was passed to the function when it was called.
If you run the tests now you'll see that our test passes.
Next, we'll check for a non-empty object using the exact same process.
App/util/__tests__/api.test.js
// ...
test('returns result if non-empty object', () => {
fetch.mockResponseOnce(JSON.stringify({ id: 1 }));
const onResponse = jest.fn();
const onError = jest.fn();
return api('/posts')
.then(onResponse)
.catch(onError)
.finally(() => {
expect(onResponse).toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled();
expect(onResponse.mock.calls[0][0]).toEqual({ id: 1 });
});
});
Again, the test should pass.
Finally, we'll write our test for the final case.
App/util/__tests__/api.test.js
// ...
test('throws an error if empty object', () => {
fetch.mockResponseOnce(JSON.stringify({}));
const onResponse = jest.fn();
const onError = jest.fn();
return api('/posts')
.then(onResponse)
.catch(onError)
.finally(() => {
expect(onResponse).not.toHaveBeenCalled();
expect(onError).toHaveBeenCalled();
});
});
This time we switch things up - we check that onResponse
is not called and onError
is called. If you run this test you'll see that it fails.
To fix the error we need to analyze the response before returning it from our API.
App/util/api.js
export const api = (path, options = {}) => {
return fetch(`https://jsonplaceholder.typicode.com${path}`, options)
.then((res) => res.json())
.then((response) => {
if (!Array.isArray(response) && Object.keys(response).length === 0) {
throw new Error('Empty Response');
}
return response;
});
};
You can see now that I check if the object is not an array and check how many keys are on it. If it's not an array and it has no keys on the object, I throw an error.
Now if you run the tests you'll see we're all green!