Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Using the React Testing Library debug method

11 min read 3264

Using The React Testing Library Debug Method

Testing your application before deploying it to production guarantees an error-free product that will function as intended. Knowing how to perform the right tests and debug your code is necessary to make you a better developer.

In this article, we’ll cover how to use the React Testing Library debug method to identify and analyze test errors. We’ll cover the following:

Later in the article, we’ll also explore debugging with the RTL logTestingPlaygroundURL() method. To follow along with this tutorial, you should be familiar with React and React Testing Library. You can find the full code for this tutorial at the GitHub repository. Let’s get started!

An overview of React Testing Library

React Testing Library, also called RTL, provides a solution for testing React components that mimics how users would interact with them. RTL’s approach avoids testing the implementation details, thereby making our test code easier to maintain. Essentially, React Testing Library is a set of utilities used alongside a test runner like Jest or Mocha.

React Testing Library vs. Jest

As a refresher, Jest is both a test runner and a testing framework for JavaScript related libraries and frameworks. Therefore, we can write tests for our React application and debug the Jest tests using Jest only.

Jest offers a few different methods to debug an application and resolve problems if something goes wrong. Depending on the environment, we can troubleshoot Jest tests using either the Node.js debugger; statement in Chrome Inspector or debugging in Visual Studio Code.

While the methods above might come in handy if we’re using only Jest, React Testing Library provides us with methods like debug(), logTestingPlaygroundURL(), and the logRoles function to simplify debugging tests. Let’s dive in and implement these methods!

Adding React Testing Library to React apps

To perform a test on a React app with RTL and identify test errors during debugging, first, we have to add RTL to our application.

React projects created with the Create React App CLI come bundled with RTL, so no additional installation is required. Otherwise, we have to include RTL using npm as follows:

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

We can also install RTL using Yarn:

yarn add --dev @testing-library/react

In addition to RTL, we’ll include the jest-dom utility, which lets us use custom matchers like .toBeInTheDocument():

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

Next, let’s clone the React project that we’ll use in this tutorial:

git clone https://github.com/Ibaslogic/react-rtl-debug 

Then, head to the project folder and generate the node_modules folder:

cd react-rtl-debug

npm install
# or
yarn

Finally, run the project:

npm run start
# or
yarn start

You should see the project open in the browser at port 3000. If it doesn’t open automatically, visit http://localhost:3000/ after successfully starting the dev server:

Initial Page Load Application Loading Message

As seen in the GIF above, the application shows a loading message on the initial page load while retrieving and displaying the list of posts from a backend server.

If you need a refresher on creating the project, check out this article on how to fetch data in a React project. The project file structure should look similar to the following code:

 project
   ...
    ├── src
    │    ├── components
    │    │      ├── App.js
    │    │      ├── Header.js
    │    │      └── Posts.js
    │    ├── app.css
    │    ├── index.js
    │    └── setupTest.js
   ...

Now, we can begin interacting with React Testing Library.

Using React Testing Library

Before we get started with the debug method, let’s explore some of the RTL facilities.

RTL provides methods that let us query the DOM nodes and make assertions about their content. In the most straightforward implementation, we’ll write a test to assert that header text reading Fetch asynchronous posts displays in the UI of our application.

In a text file called components/Header.test.js, we can add the following code:

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

test('should display heading text', () => {
  render(<Header />);
  const headingText = screen.getByText('Fetch asynchronous posts');
  expect(headingText).toBeInTheDocument();
});

The render method from React Testing Library lets us render the React component that we want to test into the testing environment. Meanwhile, the screen object provides access to query methods like getByText() to find DOM nodes.

Then, in the test block, we assert that the text in the returned DOM node is present on the page by using the toBeInTheDocument() matcher from jest-dom.

If we save the file and run the test with the npm run test command, the test should pass with the following result:

Dom Node Text Present Rtl Test Pass

Debugging test failures with the React Testing Library

Sometimes, unknowingly, we may write a test to query a DOM element that doesn’t exist. For instance, let’s modify the text inside the query we wrote previously so that it doesn’t match a particular element:

const headingText = screen.getByText(
  'Does not exist: Fetch asynchronous posts'
);

As a result of the change above, the test will fail with the following result:

Test Fail Query Nonexistent Result

Automatic logging

When the screen.getByText() method doesn’t find a matching DOM node, it throws a meaningful error message, as seen in the image above. This error contains the current state of the DOM, as highlighted in the image.



Thanks to this automatic logging when a failure occurs, it’s easier for us to visualize the DOM, giving us a hint as to why an assertion failed. The image above shows that the heading text doesn’t match what we provided in the test block. Now that we’ve located the bug, we can fix the text so that our test will pass.

One testing method called test-driven development (TDD) helps make development a breeze; TDD lets us write test cases based on product requirements before the product is fully developed.

Using the screen.debug() method

React Testing Library exposes a debug() method from the screen object to print out the state of the DOM. In addition to the automatic logging we explained above, the debug() method can also help us visualize the DOM tree before writing an assertion.

Understanding the screen.debug() syntax

Take a look at the screen.debug() syntax shown below:

screen.debug(element, maxLengthToPrint, options);

The first parameter of the debug() method is the element we want the screen.debug() method to print out. This parameter can be a single element or multiple elements. If left undefined, it will default to printing the root node.

The second parameter lets us specify the content length to print. The default output length is 7000, meaning the content will be truncated after seven thousand characters. We can increase or limit the output length as needed.

We may also want to configure test formatting using the options parameter. For instance, we can turn off syntax highlighting in the terminal with the options parameter as follows:

screen.debug(undefined, null, { highlight: false });

Example using the debug() method

Using our last test example, we’ll use the screen.debug() method to debug the document states and the heading element. First, let’s consider debugging the document states:

test('should display heading text', () => {
  render(<Header />);
  screen.debug();
  // assertion
});

Since we didn’t pass any argument to debug(), it will print the state of the DOM tree as follows:

<body>
  <div>
    <h1>
      Fetch asynchronous posts
    </h1>
  </div>
</body>

By visualizing the DOM, we can easily identify and analyze test errors, which we’ll explore later on in the article.

Next, let’s consider debugging the heading element. To log the heading element, we’ll pass the heading node to debug():

test('should display heading text', () => {
  render(<Header />);
  const headingText = screen.getByText('Fetch asynchronous posts');
  screen.debug(headingText);
  // assertion
});

Below is the output:

<h1>
  Fetch asynchronous posts
</h1>

With this output printed by the debug() method, we’re sure that the target element is present at that development stage. This will come in handy in making a proper assertion in our test.

Waiting for appearance and disappearance using debug()

Let’s further explore the React Testing Library debug() method and see how we can use it to examine the program’s state at various development stages.

Earlier in this tutorial, we saw a loading message displayed in our application while data was being fetched from the server. As soon as the data was returned, the loading message disappeared. Now, we’ll write a test for this asynchronous operation while also debugging the test code using the debug() method.

Creating our test file and checking the DOM state

To render the posts, we’re using the components/Posts.js component file. In the same directory, we’ll create a test file called Posts.test.js and add the following code:

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

test('should display loading message', () => {
  render(<Posts />);
  screen.debug();
});

If we save the file, we should see the current state of the DOM:

Test File Render Posts

The current state in the DOM contains the loading message, which was expected. At this point, the data hasn’t arrived.

Axios >0.27.2 will break Jest tests in CRA

If you’re using the latest version of Axios at the time of writing, v1.2.5, you may encounter the following error:

SyntaxError: Cannot use import statement outside a module

This Jest error is due to how newer versions of Axios emit ES modules instead of CommonJS. A workaround for this is to update the test scripts in package.json to the following:

"test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!axios)/\"",

Another option is to downgrade Axios to v0.27.2 using npm:

npm i [email protected]

Then, stop your test with Ctrl + C and rerun it with npm run test. You should now see the current state of the DOM tree.

Asserting that our text is in the document

Now that we know what React Testing Library is seeing, we can assert that the string A moment please… is present in the document. To do so, update the test block to the following code:

test('should display loading message', () => {
  render(<Posts />);
  // screen.debug();
  const loadingMessage = screen.getByText('A moment please...');
  expect(loadingMessage).toBeInTheDocument();
});

If we save the test file, the test should pass with the following result:

Asserting String Document Test Pass

Testing for appearance

Let’s perform a test to ensure that our post data returns from the server and is displayed in the client. We’ll  use the debug method to make the testing process easier.

Since we’re fetching the post data asynchronously, we have to set up our test to wait for the posts before it displays in the DOM. For such operations, RTL provides async methods like findBy* and waitFor. These methods return promises, so we’ll treat them as such by using the await keyword when calling them. The code below performs an asynchronous test using the findBy* async method:

test('should fetch and display asynchronous posts', async () => {
  render(<Posts />);
  screen.debug(); //post initially not present
  const postItemNode = await screen.findByText('qui est esse');
  screen.debug(); //post is present
});

findBy* returns a promise that will only resolve when an element is found or rejected for other cases. We’ve handled this returned promise with the await keyword to prevent the test from completing before the async promise settles. Doing so helps to avoid a false-positive failure, which is a situation where a test passes even when the application breaks.

Notice how we strategically placed the debug() method to visualize the DOM tree before and after the post data arrives. See the output below:

Visualize DOM Tree Date Before After

As we can see, the debug method helps simulate the app’s behavior. When the page initially loads, it prints a loading message. When the data returns, it replaces the loading message with the data.

Now that we’re sure that the post data has arrived, we can perform an assertion that a post item is present in the DOM:

test('should fetch and display asynchronous posts', async () => {
  render(<Posts />);
  const postItemNode = await screen.findByText('qui est esse');
  expect(postItemNode).toBeInTheDocument();
});

The test should pass with the following result:

Assert Post Item Present Dom

Avoid hitting the actual API

In practice, when performing a test, we should avoid hitting the actual API to prevent our test from becoming slow and fragile. Instead, we should create mock data to model the API interaction.

This tutorial focuses on the debug method, so we won’t cover mocking an API. However, this project’s GitHub repository implements API mocking using Mock Service Worker (MSW).

Testing for disappearance

To finish up our lesson on the React Testing Library debug method, we can test that the loading message is initially present on page load and then disappears once the post data arrives. The test code looks like the following:

test('Should display loading message and disappear when posts arrive', async () => {
  render(<Posts />);
  screen.debug(); //message initially present
  await waitForElementToBeRemoved(() =>
    screen.getByText('A moment please...')
  );
  screen.debug(); //loading message not present
});

Though not the focus of this article, RTL provides the waitForElementToBeRemoved helper function to test that an element initially appears and later disappears asynchronously. It returns a promise that will resolve when the target node is removed from the DOM.

The debug() method placement lets us visualize the DOM tree before and after the loading message disappears. Below is the output:

Debug Method Placement Visualize Dom Tree

As seen in the image above, the first debug method prints the DOM tree containing the loading message, while the second debug informs us that the message is no longer present because the data has arrived from the server.

Keep in mind that we’ve implemented a mock API using MSW to intercept the network request and return a response. In this case, it returns the title 1 text seen above instead of the API post’s actual data.

Now, the test should pass with the following result:

Debug Method Print Dom Tree Test Pass

The logRoles function

Like the debug() method, logRoles can log an element’s ARIA role or a list of roles applied to elements within the DOM tree. This process can help make testing easier, as we’ll see in a moment.

In this tutorial, we used the getByText and findByText query methods to find elements on the page. While this works, RTL places *ByRole counterparts at the top of the priority list.

To use *ByRole queries, we must be familiar with the implicit roles placed on HTML elements. When dealing with non-semantic elements, we can manually provide a role attribute. A semantic element like <button> has an implicit role of button. Feel free to check out the list of ARIA roles that apply to HTML elements.

With the logRoles function, we can easily log the element’s implicit ARIA roles for use in our accessibility test. If we revisit our previous test using findByText, we can apply the helper function to our test code like so:

import {
  // ...
  logRoles,
} from '@testing-library/react';
// ...
test('should view implicit roles with logRoles', async () => {
  render(<Posts />);
  const postItemNode = await screen.findByText('title 1');
  logRoles(postItemNode);
  expect(postItemNode).toBeInTheDocument();
});

In the code above, we started by importing logRoles from the testing library. Then, we passed the target node as an argument to the function. The output will give us the ARIA role of that element:

Logroles Target Node Output Argument Function

As seen in the image above, the ARIA role is heading. We can refactor the test code to use the findByRole accessible query instead of findByText, so we have the following:

test('should view implicit roles with logRoles', async () => {
  render(<Posts />);
  const postItemNode = await screen.findByRole('heading', {
    name: 'title 1',
  });
  expect(postItemNode).toBeInTheDocument();
});

To guarantee that our UI is accessible, we should consider using *ByRole before the other query types.

To print a list of ARIA roles applied to elements within the DOM tree, we can pass the rendered container element to logRoles as an argument. The code will look like the following:

test('should view implicit roles with logRoles', async () => {
  const view = render(<Posts />);
  const postItemNode = await screen.findByRole('heading', {
    name: 'title 1',
  });
  logRoles(view.container);
  expect(postItemNode).toBeInTheDocument();
});

The output will now look like the image below:

Print Aria Roles Dom Tree

The output, as seen above, contains the DOM elements and their respective ARIA roles. We can target these elements by their implicit roles. For instance, if we want to assert that a li item is visible in the DOM, we can write the following test:

test('should list item visible in the DOM', async () => {
  render(<Posts />);
  const postItemNode = await screen.findByRole('listitem');
  expect(postItemNode).toBeVisible();
});

Debugging with the logTestingPlaygroundURL() method

The screen object also exposes the logTestingPlaygroundURL() method, which further simplifies debugging tests. When we use this method in our test code, RTL returns a link in the editor terminal pointing to a testing playground.

If you know how the testing playground works, we can paste the DOM tree that we printed using the debug() method to find the best queries to target elements. For instance, let’s revisit our first example test. As we learned, using the debug() method will print the DOM tree:

test('should display heading text', () => {
  render(<Header />);
  screen.debug();
});

Once printed, the DOM tree should look like the following:

<div>
  <h1>
    Fetch asynchronous posts
  </h1>
</div>

Remember, in the example, we targeted the heading node and made assertions using the screen.getByText() method as follows:

test('should display heading text', () => {
  render(<Header />);
  const headingText = screen.getByText('Fetch asynchronous posts');
  expect(headingText).toBeInTheDocument();
});

While this works, as we mentioned in the logRoles section, we should consider using accessible queries before other query types. For beginners who aren’t sure of the most appropriate query methods to use, the playground can help.

If we copy the DOM elements into the testing playground (label 1), we can select the element in the view, (label 2). Then, we’ll see a suggested query, label 3, which we can copy into our test:

Copy Dom Elements Testing Playground

Now, with this query, a more accessible test will look like the following code:

test('should display heading text', () => {
  render(<Header />);
  const headingText = screen.getByRole('heading', {
    name: /fetch asynchronous posts/i,
  });
  expect(headingText).toBeInTheDocument();
});

With this playground in mind, let’s use screen.logTestingPlaygroundURL() in the test block as follows:

test('should display heading text', () => {
  render(<Header />);
  screen.logTestingPlaygroundURL();
});

Then, RTL will generate a link to the playground after we run the test:

RTL Generate Playground Link

If we visit the link, we’ll get the equivalent of the playground UI shown above. Therefore, we don’t have to use the debug() method to print and copy the DOM tree into the playground. Instead, it automatically gets added.

Without the debug() method, viewing the playground via the screen.logTestingPlaygroundURL() method lets us see at a glance what elements are visible to the user. It also suggests the most suitable query methods to target the elements.

Conclusion

Testing can be cumbersome if you’re just getting started with it. However, the ability to debug can make this process a breeze.

In this article, we discussed how to use the debug() method from React Testing Library to identify and analyze test errors. We also learned how to use the logTestingPlaygroundURL() method to further ease the debugging process. If you enjoyed this lesson, be sure to leave a comment. Happy coding!

LogRocket: Full visibility into your production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?

Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.

No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

2 Replies to “Using the React Testing Library debug method”

  1. Hi, thanks for the article! You guys make some of the best content on the web IMHO. Only slight criticism is that I haven’t used this method in months because `logTestingPlaygroundUrl` exists!

    1. Hi Ian, we have added a section addressing the `logTestingPlaygroundURL` method. Thanks for the kind words and the helpful feedback!

Leave a Reply