Mocking APIs in a React application using Jest
If you've ever tried to test a React component which calls an API on load of the component, then you will probably have found them a little complex when it comes to writing your Jest and Enzyme tests. You often find that your mocked endpoints return the incorrect status or your tests run before the mocked api call has returned the data you need. This article will attempt to simplify this.
Lets start with our React component. To keep it as simple as possible, I have used just plain JavaScript and not TypeScript. However, the solution applies to both:
import React, { useEffect, useState } from 'react'; import { hot } from 'react-hot-loader'; // services import myService from '../services/myService'; const Component = () => { const [loading, setLoading] = useState(false); const [data, setData] = useState({}); const [error, setError] = useState(''); useEffect(() => { setLoading(true); myService.GET('/my-endpoint') .then(res => { setData(res.data); setLoading(false); }) .catch(err => { setError('Unable to load data'); setLoading(false); }); }); return (
{ data ? <p className="data">{data.myData}</p> : 'no data' }
{ error ? <p className="error">{error}</p> : null }); } export default hot(module)(Component)
This is a simple component which is using React Hooks. It calls myService.GET on load of the component and then sets some React state based on the promise response. Then there's some conditional rendering in the return statement of the component which renders different P tags based on whether we receive data from the api or not.
Our unit test comes next. In our test file we have the following:
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import { mount, configure } from 'enzyme';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { act } from 'react-dom/test-utils';
configure({ adapter: new Adapter() });
import Component from './src/components/Component';
describe('Component', () => {
const mock = new MockAdapter(axios);
it('should be able to load Component and handle a 200 response correctly', async () => {
const url = 'YOUR-DOMAIN-OF-THE-ENDPOINT-HERE' + '/my-endpoint';
mock.onGet(url).reply(200, {myData: 'Some data, it doesnt matter for this example.'});
const wrapper = mount(<Component />);
await act( async () => {
await new Promise(resolve => setImmediate(resolve));
wrapper.update();
expect(wrapper.find('p.data').text()).toEqual('Some data, it doesnt matter for this example.');
});
});
it('should be able to load Component and handle an error response correctly', async () => {
const url = 'YOUR-DOMAIN-OF-THE-ENDPOINT-HERE' + '/my-endpoint';
mock.onGet(url).reply(404, {});
const wrapper = mount(<Component />);
await act( async () => {
await new Promise(resolve => setImmediate(resolve));
wrapper.update();
expect(wrapper.find('p.error').text()).toEqual('Unable to load data.');
});
});
});
In this test file you can see we have one describe with 2 tests. The first test mimics a 200 response with some mocked data and the second does the same but mimics a 404 response.
The important bits here are the async awaits and this line:
await new Promise(resolve => setImmediate(resolve));
This line makes sure our promise resolve or reject before our tests run. Then we update the wrapper to ensure when our tests run following this, that the DOM has updated and we can successfully target our elements based on the mocked data responses.