Testing Asynchronous Functionality in a React Component
Oct 15, 2019 • 9 Minute Read
Introduction
The majority of functionality in a React application will be asynchronous. Testing asynchronous functionality is often difficult but, fortunately, there are tools and techniques to simplify this for a React application.
This guide will use Jest with both the React Testing Library and Enzyme to test two simple components. The code will use the async and await operators in the components but the same techniques can be used without them.
The first component accepts a function that returns a promise as its get prop. This function is called when a button is clicked and the result that it returns is displayed. The code for this component is:
const DisplayData = ({ get }) => {
const [display, setDisplay] = React.useState(null);
const getData = async () => {
try {
const data = await get();
setDisplay(data);
} catch (err) {
setDisplay("**** ERROR ****");
}
};
return (
<>
<button type="button" onClick={getData} aria-label="get data">
Get data
</button>
{display && (
<div className="display" aria-label="display">
{display}
</div>
)}
</>);
};
In the onClick event of the button, the get function is called and, when the promise returns, the display state is either set to the result or an error message is surfaced.
The second component will wait for twenty seconds after it has been mounted and then display a message. The code for this component is:
const TimerMessage = () => {
const [message, setMessage] = React.useState(null);
React.useEffect(() => {
setTimeout(() => setMessage("Hello"), 20000);
}, []);
return (
<div>
{message && (
<div className="message" aria-label="Message">
{message}
</div>
)}
</div>);
};
The Effect hook is called with an empty array as the dependency parameter, meaning it will execute when the component is mounted. The call to setTimeout will wait for twenty seconds and then set the message state.
Testing an Asynchronous Function
To test the first component, we need to supply a mock function that will return a promise. We will use jest.fn to create two mocks: one that resolves the promise to a result and one that rejects the promise to test the error condition. The code for these mocks looks like this:
const successResult = "Some data";
const getSuccess = jest.fn(() => Promise.resolve(successResult));
const getFail = jest.fn(() => Promise.reject(new Error()));
To test the component using React Testing Library we use the render function, passing one of the mock functions as the get prop and use object destructuring to get the getByLabelText and queryByLabelText functions from the return value. Firstly, we use queryByLabelText to try and get the div used to display the results; this should be null at the moment as the display state has not yet been set:
const { getByLabelText, queryByLabelText } = render(<DisplayData get={getSuccess} />);
const labelBeforeGet = queryByLabelText(/display/i);
expect(labelBeforeGet).toBeNull();
Then we fire a click event on the button in order to call the get function. This will eventually set the display state and update the div; however, if we try and get that div straight away it will still be null as the code is waiting for the get promise to return. To wait for this we can use the waitForElement function which, as its name suggests, waits until the element exists in the DOM before it returns; in fact it waits for up to four seconds and, if the element still doesn't exist, then throws an error. Once the element exists we can then test if it contains the results or the error message, depending on which mock was passed in:
const button = getByLabelText(/get data/i);
fireEvent.click(button);
const labelAfterGet = await waitForElement(() => queryByLabelText(/display/i));
expect(labelAfterGet.textContent).toEqual(successResult);
The code for these tests is here.
Testing this component with Enzyme is similar; the only real difference being that there is no equivalent to the waitForElement function meaning that we need to do something different when waiting for the component to update.
Firstly, we use shallow rendering to render the component, again using one of the two mocks as the get prop:
const wrapper = shallow(<DisplayData get={getSuccess} />);
As in the previous example, we verify that the display div does not exist before the button click:
const displayDivBeforeClick = wrapper.find(".display");
expect(displayDivBeforeClick.exists()).toBe(false);
The we simulate a button click:
const getButton = wrapper.find("button");
getButton.simulate("click");
As discussed previously, Enzyme has no way to wait for an element to be added. So, to make the promise return, we can use the setImmediate function and then can test the component after it returns:
return new Promise(resolve => setImmediate(resolve)).then(() => {
const displayDivAfterClick = wrapper.find(".display");
expect(displayDivAfterClick.exists()).toBe(true);
expect(displayDivAfterClick.text()).toEqual(successResult);
});
The code for these tests is here.
Testing a Timer
To test the second component, we could write a test that waits for twenty seconds and then verifies that the state has been updated but it is generally bad practice to write tests that take that long to execute. To ensure the tests run in an acceptable time, we can use jest fake timers which will allow the test to make the setTimeout execute the callback immediately.
Firstly, we need to call jest.useFakeTimers() to ensure we are using fake timers. Then, we can create the component using React Testing Library:
const { queryByLabelText } = render(<TimerMessage />);
When testing this component in Enzyme, we cannot use the shallow rendering as with the previous component. The timer will only execute if the component is actually mounted into a DOM which is done using the mount function:
const wrapper = mount(<TimerMessage />);
Next we need to force the timer to complete and execute the callback; we do this by calling jest.runAllTimers(). The callback should now have updated the state and, therefore, the message should be showing. Verify this in React Testing Library:
const afterTimer = queryByLabelText(/message/i);
expect(afterTimer.textContent).toEqual("Hello");
And in Enzyme:
const afterTimer = wrapper.text();
expect(afterTimer).toBe("Hello");
The code for the React Testing Library test is here and for the Enzyme test here.
Conclusion
Testing asynchronous functionality can sometimes be difficult but Jest combined with either React Testing Library or Enzyme makes this a much simpler task.
All of the code for this guide can be found here.