Author avatar

Chris Dobson

Testing Asynchronous Functionality in a React Component

Chris Dobson

  • Oct 15, 2019
  • 9 Min read
  • 49,005 Views
  • Oct 15, 2019
  • 9 Min read
  • 49,005 Views
Web Development
Front End Web Development
Client-side Framework
React

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:

1const DisplayData = ({ get }) => {
2  const [display, setDisplay] = React.useState(null);
3
4  const getData = async () => {
5    try {
6      const data = await get();
7      setDisplay(data);
8    } catch (err) {
9      setDisplay("**** ERROR ****");
10    }
11  };
12  return (
13    <>
14      <button type="button" onClick={getData} aria-label="get data">
15        Get data
16      </button>
17      {display && (
18        <div className="display" aria-label="display">
19          {display}
20        </div>
21       )}
22    </>);
23};
javascript

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:

1const TimerMessage = () => {
2  const [message, setMessage] = React.useState(null);
3
4  React.useEffect(() => {
5    setTimeout(() => setMessage("Hello"), 20000);
6  }, []);
7
8  return (
9    <div>
10      {message && (
11        <div className="message" aria-label="Message">
12          {message}
13        </div>
14      )}
15    </div>);
16};
javascript

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:

1const successResult = "Some data";
2const getSuccess = jest.fn(() => Promise.resolve(successResult));
3const getFail = jest.fn(() => Promise.reject(new Error()));
javascript

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:

1const { getByLabelText, queryByLabelText } = render(<DisplayData get={getSuccess} />);
2const labelBeforeGet = queryByLabelText(/display/i);
3expect(labelBeforeGet).toBeNull();
javascript

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:

1const button = getByLabelText(/get data/i);
2fireEvent.click(button);
3const labelAfterGet = await waitForElement(() => queryByLabelText(/display/i));
4
5expect(labelAfterGet.textContent).toEqual(successResult);
javascript

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:

1const wrapper = shallow(<DisplayData get={getSuccess} />);
javascript

As in the previous example, we verify that the display div does not exist before the button click:

1const displayDivBeforeClick = wrapper.find(".display");
2expect(displayDivBeforeClick.exists()).toBe(false);
javascript

The we simulate a button click:

1const getButton = wrapper.find("button");
2getButton.simulate("click");
javascript

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:

1return new Promise(resolve => setImmediate(resolve)).then(() => {
2  const displayDivAfterClick = wrapper.find(".display");
3
4  expect(displayDivAfterClick.exists()).toBe(true);
5  expect(displayDivAfterClick.text()).toEqual(successResult);
6});
javascript

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:

1const { queryByLabelText } = render(<TimerMessage />);
javascript

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:

1const wrapper = mount(<TimerMessage />);
javascript

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:

1const afterTimer = queryByLabelText(/message/i);
2expect(afterTimer.textContent).toEqual("Hello");
javascript

And in Enzyme:

1const afterTimer = wrapper.text();
2expect(afterTimer).toBe("Hello");
javascript

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.