How to Use React Testing Library to Wait for Async Elements, a Step-by-Step Guide

updated on 30 November 2022

In this post, you will learn about how JavaScirpt runs in an asynchronous mode by default. You will also get to know about a simple React.js app that fetches the latest Hacker News front page stories. You will write tests for the asynchronous code using React Testing Library watiFor function and its other helper functions in a step-by-step approach. Let’s get started!

JavaScript is Asynchronous by Default

JavaScript is a complicated language, like other popular languages it has its own share of quirks and good parts. JavaScript is a single-threaded and asynchronous language which is a commendable but not so easy-to-understand feature. You might be wondering what asynchronous means. If tasks are executed one after the other where each task waits for the previous task to complete, then it is synchronous. If the execution can switch between different tasks without waiting for the previous one to complete it is asynchronous.

Suppose you have a function with 5 lines of code. If it is executed sequentially, line by line from 1 to 5 that is synchronous. If line 2 is put in the background and then line 3 is executed, then when line 4 is executing the result of line 2 is available this is asynchronous. Javascript can run on the asynchronous mode by default. Pushing the task in the background and resuming when the result is ready is made possible by using events and callbacks. The event can be all data received which triggers a callback to process the received data. A better way to understand async code is with an example like below:

console.log('First log message');

// 1 second wait
setTimeout(function(){
  console.log('Third log message - after 1 second');
}, 1000);

console.log('Second log message');

If the above code would execute sequentially (sync) it would log the first log message, then the third one, and finally the second one. But the output will be as follows:

First log message'
Second log message'
Third log message - after 1 second

This is where the power of async programming is evident. Line 1 is executed first, then line 3 was executed but pushed in the background with setTimeout with an instruction to execute the code within setTimeout after 1 second. The code execution moved forward and the last console.log in the script printed Second log message. After one second passed, the callback is triggered and it prints the Third log message console log. This is managed by the event loop, you can learn more about the JavaScript event loop in this amazing talk. You can also step through the above code in this useful visualizer to better understand the execution flow.

You can learn more about this example where the code waits for 1 second with Promises too. All external API calls can also be dealt with in an async way using Promises and the newer async/await syntax.

In terms of testing, the async execution model is important because the way any asynchronous code is tested is different from the way you test synchronous sequential code. For any async code, there will be an element of waiting for the code to execute and the result to be available. You will learn about this in the example app used later in this post. In the next section, you will learn more about React Testing library.

React Testing Library

React testing library (RTL) is a testing library built on top of DOM Testing library. It is built to test the actual DOM tree rendered by React on the browser.

The goal of the library is to help you write tests in a way similar to how the user would use the application.

This approach provides you with more confidence that the application works as expected when a real user uses it. This is based on their guiding principle:

The more your tests resemble the way your software is used, the more confidence your tests will give you.

The end user doesn’t care about the state management library, react hooks, class, or functional components being used. They want your app to work in a way to get their work done. Within that context, with React Testing Library the end-user is kept in mind while testing the application. This user-centric approach rather than digging into the internals of React makes React Testing Library different from Enzyme. Enzyme was open-sourced by Airbnb at the end of 2015. It was popular till mid-2020 but later React Testing library became more popular than Enzyme. React Testing Library’s rise in popularity can be attributed to its ability to do user-focused testing by verifying the actual DOM rather than dabbling with React.js internals.

React Testing Library is written by Kent C. Dodds. Kent is a well-known personality in the React and testing space. React testing library became more popular than Enzyme in mid-Sep 2020 as per NPM trends. Currently, RTL has almost 7 million downloads a week on NPM. In the next section, you will see how the example app to write tests using React Testing Library for async code works.

Example App to get HackerNews Stories

For this guide to use React Testing Library waitFor, you will use a React.js app that will get the latest stories from the HackerNews front page. To fetch the latest stories from HN you will use the unofficial HackerNews API provided by Aloglia. This example app is created using Create React App (CRA) and the HackerNews component has the following code:

import { useState, useEffect } from 'react';

const HackerNewsStories = () => {
  const [stories, setStories] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchStories = async () => {
      try {
        const data = await (await fetch('https://hn.algolia.com/api/v1/search_by_date?tags=front_page&hitsPerPage=20')).json();
        setStories(
          data.hits.sort((story, nextStory) => (story.points < nextStory.points ? 1 : -1))
        );
        setError(null);
      } catch (err) {
        setError(err.message);
        setStories(null);
      } finally {
        setLoading(false);
      }
    };
    fetchStories();
  }, []);

  return (
    <div className="wrapper">
      <h2>Latest HN Stories</h2>
      {loading && <div>HackerNews frontpage stories loading...</div>}
      {error && <div>{`Problem fetching the HackeNews Stories - ${error}`}</div>}
      <div className="stories-wrapper">
        {stories &&
          stories.map(({ objectID, url, title, author, points }) => (
            title && url &&
            <div className='stories-list' key={objectID}>
              <h3><a href={url} target="_blank" rel="noreferrer">{title}</a> - By <b>{author}</b> ({points} points)</h3>
            </div>                        
          ))}
      </div>
    </div>
  );
};

export default HackerNewsStories;

You are adding a basic react component that pulls in the latest front-page stories from HackerNews using the unofficial API provided by Algolia. Like most modern React components using hooks this one also starts by importing setState and useEffect hook.

Next, you define a function called HackerNewsStories that houses the whole Hacker News stories component. Three variables, stories, loading, and error are set with initial empty state using setState function. After that, the useState hook is defined. This function pulls in the latest Hacker News front page stories using the API. Then, it sorts the stories with the most points at the top and sets these values to the stories variable with the setStories function call. If there are no errors the error variable is set to null.

In case of any error, the code goes to the catch block where the error is set to the message of the caught error, then the stories variable is set to null. In both error or no error cases the finally part is executed setting the loading variable to false which will remove the div showing the stories are being loaded message.

After this, it returns the function with the JSX, which will be rendered as HTML by the browser. The output is also simple, if the stories are still being loaded it will show the loading div with the text HackerNews frontpage stories loading... else it will hide the loading message. The same logic applies to showing or hiding the error message too.

The main part here is the div with the stories-wrapper class. In this div, If stories exist, each story title will be rendered in an h3 tag with a link to the story. The author and the points of the story are printed too. It is a straightforward component used in the App component with <HackerNewsStories />. The whole code is available as a GitHub repository if you want to further dissect the code.

The output looks like the below or you can see a working version on Netlify if you like:

react-testing-library-waitfor-example-app-0o1n7

In the next segment, you will add a test for the above app and mock the API call with a stubbed response of 2 stories.

Testing the App with React Testing Library

If you have used Create React App to set up the React.js application you will not need to install the React testing library. If you have set up React.js without the React Testing library you can run the following commands to get the needed NPM packages for testing with React Testing Library:

npm install --save-dev @testing-library/react @testing-library/jest-dom

The Jest DOM npm package is needed to use custom matchers like .toBeInTheDocument() and .toHaveAccessibleName(), etc. Next, you will write the test to see the component is rendering as expected.

Simple Test Mocking out window.fetch Response

Given you have all the necessary packages installed, it is time to write a simple test using React Testing Library:

import { render, screen } from '@testing-library/react';
import HackerNewsStories from './HackerNewsStories';

test('should render latest HN stories H2', async () => {
  render(<HackerNewsStories />);
  screen.debug();
});

This will print the current output when the test runs. You can understand more about debugging React Testing library tests and also find out about screen.debug and prettyDOM functions. These functions are very useful when trying to debug a React testing library test.

When it runs, it will show a CLI output like the below:

console.log
    <body>
      <div>
        <div
          class="wrapper"
        >
          <h2>
            Latest HN Stories
          </h2>
          <div>
            HackerNews frontpage stories loading...
          </div>
          <div
            class="stories-wrapper"
          />
        </div>
      </div>
    </body>

    /<path-to-project>/src/HackerNewsStories.basic.test.js:6:10
      4 | test('should render latest HN stories H2', async () => {
      5 |   render(<HackerNewsStories />);
    > 6 |   screen.debug();

As the real API is being called for this test, it is ok for quick and dirty debugging. It is not ideal to run it many times or run it as part of a CI/CD pipeline. Making a test dependent on an external resource like an API can make the test flaky and cause unnecessary requests to the API too. For these reasons, your unit tests should never use any external resource like the network or even the file system. To solve this issue, in the next step, you will mock the API call by using Jest SpyOn. The new test code will look like the following code which mocks the API call:

import { render, screen } from '@testing-library/react';
import HackerNewsStories from './HackerNewsStories';

let windowFetchSpy;

function wait(milliseconds){
  return new Promise(resolve => {
      setTimeout(resolve, milliseconds);
  });
}

const mockHnResponse = {
  'hits': [
    {
      'created_at': '2022-10-01T20:26:19.000Z',
      'title': 'TikTok tracks you across the web, even if you don’t use the app',
      'url': 'https://www.consumerreports.org/electronics-computers/privacy/tiktok-tracks-you-across-the-web-even-if-you-dont-use-app-a4383537813/',
      'author': 'bubblehack3r',
      'points': 123,
      'objectID': '33049774',
    },
    {
      'created_at': '2022-10-01T15:16:02.000Z',
      'title': 'The self-taught UI/UX designer roadmap (2021)',
      'url': 'https://bootcamp.uxdesign.cc/the-self-taught-ui-ux-designer-roadmap-in-2021-aa0f5b62cecb',
      'author': 'homarp',
      'points': 253,    
      'objectID': '33047199',
    },    
  ],
  'page': 0,
  'query': '',
};

let mockFetch = async (url) => {
  if (url.startsWith('https://hn.algolia.com/') && url.includes('front_page')) {
    await wait(70);
    return {
      ok: true,
      status: 200,
      json: async () => mockHnResponse,
    };
  }
}


beforeEach(() => {
  windowFetchSpy = jest.spyOn(window, 'fetch').mockImplementation(mockFetch);
})

afterEach(() => {
  jest.restoreAllMocks();
});

test('should render latest HN stories H2', async () => {
  render(<HackerNewsStories />);
  const latestStoriesH2 = await screen.getByText('Latest HN Stories');
  expect(latestStoriesH2).toBeInTheDocument();
});

You have added a Jest spyOn to the window.fetch function call with a mock implementation. This mock implementation checks if the URL passed in the fetch function call starts with https://hn.algolia.com/ and has the word front_end. If both checks pass, it will send back a stubbed response with 2 stories defined in the mockHnResponse constant.  To test the loading div appears you have added the wait with a promise. To mock the response time of the API a wait time of 70 milliseconds has been added. Another way to make this API call can be with Axios, bare in mind Fetch and Axios have their differences though.

React Testing Library (RTL) is the defacto testing framework for React.js. It also comes bundled with the popular Create React app toolchain. React Testing library is also very useful to test React components that have asynchronous code with waitFor and related functions.

The test uses Jest beforeEach hook to spy on the window.fetch before each test. It also uses the afterEach hook to restore the mock after every test.

Take note that only the happy case of the API returning the latest front-page stories is included in the stub, it will be enough for the scope of this tutorial. There won’t be test coverage for the error case and that is deliberate. Line 17-18 of the HackerNewsStories component will not be covered by any tests which is the catch part in the code.

If you rerun the tests, it will show the same output but the test will not call the real API instead it will send back the stubbed response of 2 stories.

The test checks if the H2 with the text Latest HN Stories exists in the document and the test passes with the following output:

 PASS  src/HackerNewsStories.test.js
  ✓ should render latest HN stories H2 (12 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.376 s, estimated 1 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

Great! You have your first test running with the API call mocked out with a stub. In the next section, you will test for the stories to appear with the use of React Testing library waitFor.

Testing for the Stories to Appear

As seen in the code and above image, the Hacker News React.js app first shows a “loading” message until the stories are fetched from the API. After that, it shows the stories sorted by the highest points at the top. This triggers a network request to pull in the stories loaded via an asynchronous fetch. For the test to resemble real life you will need to wait for the posts to display. This is where the React testing library waitFor method comes in handy.

The waitFor method returns a promise and so using the async/await syntax here makes sense. Alternatively, the .then() syntax can also be used depending on your preference. For this tutorial’s tests, it will follow the async/await syntax. The test to check if the stories are rendered properly looks like the below:

test('should show stories after they are fetched', async () => {
  render(<HackerNewsStories />);
  expect(windowFetchSpy).toHaveBeenCalled();
  expect(windowFetchSpy).toHaveBeenCalledWith('https://hn.algolia.com/api/v1/search_by_date?tags=front_page&hitsPerPage=20');

  //expect(screen.getByText('The self-taught UI/UX designer roadmap (2021)')).toBeInTheDocument(); //this will fail
  await waitFor(() => {
    expect(screen.getByText('The self-taught UI/UX designer roadmap (2021)')).toBeInTheDocument();
    expect(screen.getByText('homarp')).toBeInTheDocument();
  });  
});

Please take note that the API calls have already been mocked out in the previous section resulting in this test using the stubbed responses instead of the real API response. It is a straightforward test where the HackerNewsStories component is rendered first. Then, the fetch spy is expected to be called and it is called with the desired API URL.

The first commented expect will fail if it is uncommented because initially when this component loads it does not show any stories. It will be showing the loading message. That is why you are using React Testing Library waitFor method. It will wait for the text The self-taught UI/UX designer roadmap (2021) to appear on the screen then expect it to be there. The test will do the same process for the username of homarp.

The default interval for waitFor is 50 milliseconds (ms) and it has a default timeout of 1000 ms (1 second) as per its documentation. In the above test, this means if the text is not found on the screen within 1 second it will fail with an error. That will not happen as the stubbed response will be received by the call in 70 milliseconds or a bit more as you have set it in the wait in the fetch spy in the previous section.

Another way to test for appearance can be done with findBy queries, for example, findByText which is a combination of getBy and waitFor. It is discussed in a bit more detail later. For example the following expect would have worked even without a waitFor:

expect(await screen.findByText('The self-taught UI/UX designer roadmap (2021)')).toBeInTheDocument();

When writing tests do follow the frontend unit testing best practices, it will help you write better and maintainable tests. In the subsequent section, you will learn how to test for the loading message to disappear as the stories are loaded from the API.

Testing for the Loading Message to Disappear

Similar to testing an element that has appeared on the screen after the execution of a dependent asynchronous call, you can also test the disappearance of an element or text from the component. In the context of this small React.js application, it will happen for the div with the loading message. As seen above in the image, the div with the loading message will show up for a split second (or less depending on the network speed and how fast the API responds) and disappear if the API response is received without any problem.

This scenario can be tested with the code below:

test('should show and then hide the loading message when stories load', async() => {
  render(<HackerNewsStories />);
  const loadingText = screen.getByText('HackerNews frontpage stories loading...');
  expect(loadingText).toBeInTheDocument();
  expect(windowFetchSpy).toHaveBeenCalled();

  // await waitFor(() => {
  //   const loadingText = screen.queryByText('HackerNews frontpage stories loading...');
  //   expect(loadingText).not.toBeInTheDocument();
  // });

  await waitForElementToBeRemoved(() => screen.getByText('HackerNews frontpage stories loading...'), {timeout: 75});
});

As seen above, you have rendered the HackerNewsStories component first. Then, an expect assertion for the loading message to be on the screen. After that, an expect assertion for the fetch spy to have been called. Testing for an element to have disappeared can be done in two ways. The first way is to put the code in a waitFor function. With this method, you will need to grab the element by a selector like the text and then expect the element not to be in the document. This first method is commented out in the above test where the element is queried by text.

Another way to do it is with waitForElementToBeRemoved which is a convenience over the waitFor method discussed above. With this shortcut method, it can be done in a single line as seen above. The element is grabbed with getByText and as waitForElementToBeRemoved returns a promise, an await is added to make that the given element is no longer on screen.

An important detail to notice here is you have passed a timeout of 75 milliseconds which is more than the set 70 milliseconds on the stub. This is important as the stub will respond in 70 milliseconds, if you set the timeout to be less than 70 this test will fail.

Congrats! You have written tests with both waitFor to test an element that appears on screen and waitForElementToBeRemoved to verify the disappearance of an element from the component. In the next section, you will learn more about the useful findBy method to test async code with React Testing Library.

More Usage of the findBy Method

The findBy method was briefly mentioned in the above section about the stories appearing after the async API call. It can be used to deal with asynchronous code easily. As mentioned it is a combination of getBy and waitFor which makes it much simpler to test components that don’t appear on the screen up front. These components depend on an async operation like an API call.

To see more usage of the findBy method you will test that the sorting of the Hacker News stories by points where the maximum points appear on top works as expected. For this you will write a test as follows:

test('should show stories sorted by maximum points first', async() => {
  render(<HackerNewsStories />);
  expect(windowFetchSpy).toHaveBeenCalled();

  const stories = await screen.findAllByRole('heading', {level : 3});
  expect(stories).toHaveLength(2);
  expect(stories[0]).toHaveAccessibleName('The self-taught UI/UX designer roadmap (2021) - By homarp (253 points)');
  expect(stories[1]).toHaveAccessibleName('TikTok tracks you across the web, even if you don’t use the app - By bubblehack3r (123 points)');
});

In the above test, first, the HackerNewsStories component is rendered. Then the fetch spy is expected to be called. After that, in the stories const the H3 elements are fetched. It is expected that there will be 2 stories because the stubbed response provides only 2. In the stubbed response, the story with 123 points appears above the story with 253 points. As per the sorting logic in the component, the story with 253 points should come first then the story with 123 points. That is the expected output as the first story story [0] is the one with 253 points.

The important part here is waitFor is not used explicitly. In place of that, you used findByRole which is the combination of getBy and waitFor. So the H3 elements were pulled in as they became visible on screen after the API responded with a stub’s delay of 70 milliseconds. There was no use of any explicit timeout but the test still passed verifying the expected behavior.

Conclusion

In this post, you learned about the asynchronous execution pattern of JavaScript which is the default one. Then you were introduced to the HackerNews React.js application that fetches the latest front page stores of HackerNews using the API provided by Algolia. After that, you learned about various methods to test asynchronous code using React Testing Library like waitFor and findBy.

You can write a test for asynchronous code even without using waitFor by utilizing the other helper functions like findBy and waitForElementToBeRemoved. These helper functions use waitFor in the background. 

In the process, you also mocked the API call with a stub injected with Jest’s spyOn helper and a fake wait of 70 milliseconds. This guide has helped you understand how to test any React component with async code. As a reminder, all the code is available in this GtiHub repository. Carry on writing those tests, better tests add more confidence while shipping code!

Meticulous

Meticulous is a tool for software engineers to catch visual regressions in web applications without writing or maintaining UI tests.

Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.

Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.

Learn more here.

Authored by Geshan Manandhar

Read more