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};
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};
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.
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()));
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();
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);
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} />);
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);
The we simulate a button click:
1const getButton = wrapper.find("button");
2getButton.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:
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});
The code for these tests is here.
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 />);
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 />);
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");
And in Enzyme:
1const afterTimer = wrapper.text();
2expect(afterTimer).toBe("Hello");
The code for the React Testing Library test is here and for the Enzyme test here.
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.