A Beginner’s Guide to Debugging React Testing Library Tests

updated on 30 November 2022

Automated testing is crucial for the success of medium and large projects, which are often maintained for years. It is more scalable than manual testing. Visual regression testing is a category of automated testing that helps you to make sure that all the UI elements work as expected after any changes.

React.js is one of the most popular JavaScript frontend libraries and React testing library is a lightweight solution for testing React.js components. If you use create react app to create your project, react testing library is included out of the box. In this post, you will learn about the importance of automated testing, reasons to do visual regression testing, and ways to debug tests written with React Testing Libary - let’s get going!

Automated Testing is Important

Having some form of automated testing is crucial for the long-term maintainability of a project. Unit tests are the most predictable, repeatable, and fastest breed of automated tests. You write tests so that you don’t need to debug any pesky issues in the future. However, the test code is code too, so you will also need to debug the test code!

Writing tests for your code is important, the other more important aspect is writing testable code. Writing code in a testable way is a forcing function for writing clean code with best practices like dependency injection baked in. Then, adding tests on code that is already well designed makes it more maintainable and adaptable for future changes. The success of a large project that will be operated for years by multiple software engineers will rely heavily on having useful and carefully designed automated tests.

Why do Visual Regression Testing

For front-end applications, like the ones built with React.js, testing is paramount. As the frontend application is the gateway between the user and your data, the User Interface (UI) elements always need to look and behave as intended. This is where visual regression testing becomes even more crucial.

The problem of visual regression exacerbates as the size of the codebase grows with time. For a smaller codebase, most relations are easier to remember. In smaller codebases, the visual regression is relatively manageable. The full codebase and its intricacies are easily visible. But, as the size of the codebase increases, the complexity of the relationship between components also increases. It is more likely to see visual regression problems in large and legacy code bases compared to smaller and newer micro frontend projects.

Visual regression testing is a way to verify that changes do not negatively impact the visual appearance of the existing user interface of the application. For instance, an engineer changes a text box for the search form but as the text box is used in the login form too, the change breaks the login form. This is the pressing reason to do visual regression testing so that changes in one part or lower level primitive do not adversely affect other parts of the application.

Given test code is also code, it also needs to be debugged so that tests can be written easily and accurately. In the next section, you will learn about an example app to debug tests written with React testing library.

Example App - Name to Nationality Percent

To debug some React testing library tests you will use the Name to Nationality guessing application. If you are interested in how the application is built, please read the post on Jest SpyOn. It is a simple yet useful app where the first name of a person is entered and the app tries to guess the nationality percent of the given name using the Nationalize.io API. For instance, with the name chris the app works like the below:

reactjs-testing-library-debug-example-app-i42fv

This application code is available on GitHub if you want to fiddle around with the code. You can quickly try your own name as the app is hosted on Netlify. Give it a shot. Next, you will learn how to debug the existing test of the flags being displayed correctly for the React testing library test.

Example Test - Get Nationality Flag

The app already has some test cases to verify that it works as expected. One of those tests is named should get nationalities for a name checks that the happy path works fine. You will be using this test as an example to debug a react testing library test. For the getRoles part, you will use the render function on the App component as it makes more sense. In the next section, you will learn about screen.debug, which is very useful to view the current HTML output of a component.

Using screen.debug to Peek into Rendered HTML

There are a few tests in the example application. One of them is the React testing library test to check the happy path of the flags loading when a Name is entered. This test looks like the below:

test('should get nationalities for a name', async() => {
  render(<App />);

  //simulate filling up the textbox
  const personNameInput = screen.getByRole('textbox');
  fireEvent.change(personNameInput, {target: {value: 'john'}})
  expect(personNameInput.value).toBe('john');

  //click the button
   const getNationalitiesBtn = screen.getByRole('button', { name: 'Get Nationalities' });
   expect(getNationalitiesBtn).not.toBeDisabled();
   userEvent.click(getNationalitiesBtn);

   //Verify percent and flag images are displayed
   expect(await screen.findByText('3 guess(es) found')).toBeVisible();

   expect(windowFetchSpy).toHaveBeenCalled();
   expect(windowFetchSpy).toHaveBeenCalledWith('https://api.nationalize.io/?name=john');
   expect(screen.getByText('US - 4.84%')).toBeVisible();
   expect(screen.getByText('IM - 4.44%')).toBeVisible();
   expect(screen.getByText('IE - 4.21%')).toBeVisible();

   const flagImages = screen.getAllByRole('img');
   expect(flagImages).toHaveLength(3);
   expect(flagImages[0]).toHaveAccessibleName('US flag');
   expect(flagImages[1]).toHaveAccessibleName('IM flag');
   expect(flagImages[2]).toHaveAccessibleName('IE flag');
});

Let’s suppose you did not know that there were 3 flags or the country names being rendered. It could be debugged with debug method on the screen object as follows:

const flagImages = screen.getAllByRole('img');
//console.log(flagImages);
screen.debug(flagImages);

expect(flagImages).toHaveLength(3);
expect(flagImages[0]).toHaveAccessibleName('US flag');
expect(flagImages[1]).toHaveAccessibleName('IM flag');
expect(flagImages[2]).toHaveAccessibleName('IE flag');

Like any other debug process, your first tool in the debugging toolbox would be the trustworthy console.log. If the above console.log was not commented the output would have been something like:

console.log
    [
      <ref *1> HTMLImageElement {
        '__reactFiber$spdd5vndmz9': FiberNode {
          tag: 5,
          key: null,
          elementType: 'img',
          type: 'img',
          stateNode: [Circular *1],
          return: [FiberNode],
          child: null,
          sibling: null,
          index: 2,
          ref: null,
          pendingProps: [Object],
          memoizedProps: [Object],
          updateQueue: null,
          memoizedState: null,
          dependencies: null,
          mode: 1,
          flags: 1048580,
          subtreeFlags: 0,
          deletions: null,
          lanes: 0,
          childLanes: 0,
          alternate: null,
          actualDuration: 0,
          actualStartTime: -1,
          selfBaseDuration: 0,
          treeBaseDuration: 0,
          _debugSource: [Object],
          _debugOwner: [FiberNode],
          _debugNeedsRemount: false,
          _debugHookTypes: null
        },
        '__reactProps$spdd5vndmz9': {
          src: 'https://flagcdn.com/w160/us.jpg',
          alt: 'US flag',
          style: [Object]
        },
        '__reactEvents$spdd5vndmz9': Set(2) { 'error__bubble', 'load__bubble' },
        [Symbol(SameObject caches)]: [Object: null prototype] {
          style: [CSSStyleDeclaration],
          childNodes: NodeList {}
        }
      },
…
]

As seen above the console.log output for a getAllByRole call on screen is cryptic, to say the least. Rather than helping to debug it is making the process more difficult. Thereby, the generally useful console.log is not very useful in this particular case. On the other hand the output of the helpful screen.debug call is as follows:

console.log
    <img
      alt="US flag"
      src="https://flagcdn.com/w160/us.jpg"
      style="border: 1px solid black;"
    />

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)
          at Array.forEach (<anonymous>)

console.log
    <img
      alt="IM flag"
      src="https://flagcdn.com/w160/im.jpg"
      style="border: 1px solid black;"
    />

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)
          at Array.forEach (<anonymous>)

console.log
    <img
      alt="IE flag"
      src="https://flagcdn.com/w160/ie.jpg"
      style="border: 1px solid black;"
    />

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)
          at Array.forEach (<anonymous>)

Now, it is much easier for you to know that there are 3 elements in the flagImages array and each of those is a simple img tag. The next step is to check the alt text which can be tested by using toHaveAccessibleName and using the string value like US flag.

Now it is clear that the screen.debug method allows you to see the rendered HTML of the component which makes writing tests much easier. Without the debug method it might feel like shooting in the dark, especially if the component renders a complicated HTML. Next up you will learn about the magic sauce behind the debug method is prettyDOM.

Debugging DOM Elements with prettyDOM

PrettyDOM is built on top of pretty-format. The prettyDOM function is used to print out a readable representation of the DOM tree of a node.

The method used in the previous section screen.debug is a shorthand for writing console.log(prettyDOM(baseElement)). In this console.log call with prettyDOM, the baseElement is the DOM node where your React Element is rendered. You can use prettyDOM in the above happy path test case as:

const flagImages = screen.getAllByRole('img');
let index = 1;
for(const flagImage of flagImages) {    
  console.log(`flag image ${index}: `, prettyDOM(flagImage));
  index++;
}

expect(flagImages).toHaveLength(3);
expect(flagImages[0]).toHaveAccessibleName('US flag');
expect(flagImages[1]).toHaveAccessibleName('IM flag');
expect(flagImages[2]).toHaveAccessibleName('IE flag');

Notice here, that you have put a for loop explicitly to repeat through each flag image. In place of screen.debug the underlying prettyDOM function has been called in this case. PrettyDOM as the name suggests will print out the DOM elements in a neat and clean format. You can also use the pretty-format options to leverage the full powers of prettyDOM. It will give an output as follows when you run the above test:

console.log
    flag image 1:  <img
      alt="US flag"
      src="https://flagcdn.com/w160/us.jpg"
      style="border: 1px solid black;"
    />

      at Object.<anonymous> (src/App.test.js:77:13)

console.log
    flag image 2:  <img
      alt="IM flag"
      src="https://flagcdn.com/w160/im.jpg"
      style="border: 1px solid black;"
    />

      at Object.<anonymous> (src/App.test.js:77:13)

console.log
    flag image 3:  <img
      alt="IE flag"
      src="https://flagcdn.com/w160/ie.jpg"
      style="border: 1px solid black;"
    />

      at Object.<anonymous> (src/App.test.js:77:13)

The above output is pretty similar to screen.debug. In this instance, as console.log( flag Image ${index}:, prettyDOM(flagImage)); is used in a loop you have more control over how to show the output. For example, if you want to turn off the code highlight it can be done with prettyDOM(flagImage, null, {highlight:false}). The second parameter is the maxLength which is sent as null and the third parameter is the options object. The options object is the same as the options for the pretty-format package. In this particular call, you have set it turn of code highlighting with highlight: false.

Hence, the magic sauce of screen.debug is that it uses prettyDOM to do the heavy lifting. screen.debug uses prettyDOM with the default options, if you want to customize it you can call prettyDOM directly as seen in the above example. In the next part, you will learn about logRoles for easier querying of the DOM elements.

Finding Ways to Query DOM Elements using logRoles

The logRoles helper function is another useful function. It is utilized to print out a list of all the implicit ARIA roles within a tree of DOM nodes. It lists all the nodes which match that role. This is very handy to find out the ways to query the DOM being testing. Sounds a bit confusing, but the code below will clear your confusion with the app component’s roles rendered with logRoles at the start of the test as seen below:

const { container } = render(<App />);
logRoles(container);

This will output all the roles/HTML elements when the App component is initially loaded which yields:

console.log
banner:

Name "":
<header
  class="App-header"
/>

--------------------------------------------------
heading:

Name "Check Name's Nationalities percent":
<h2 />

--------------------------------------------------
form:

Name "":
<form
  name="nationalities-form"
/>

--------------------------------------------------
textbox:

Name "":
<input
  name="personName"
  placeholder="Enter a person's name"
  type="text"
  value=""
/>

--------------------------------------------------
button:

Name "Get Nationalities":
<button />

--------------------------------------------------

  at logRoles (node_modules/@testing-library/dom/dist/role-helpers.js:234:20)

This reveals there are multiple elements like one H2 with the name “Check Name's Nationalities percent”, a button element with the name “Get Nationalities” and others. With the above information, it makes it much easier to write a test like:

const getNationalitiesBtn = screen.getByRole('button', { name: 'Get Nationalities' });
expect(getNationalitiesBtn).not.toBeDisabled();
userEvent.click(getNationalitiesBtn);

As you know there is a button element and it has the name “Get Nationalities”, another test can be to check that the button is not disabled. Selecting the button by role was much easier as you could see the role printed on the console with the logRoles helper function with the container from the rendered App component.

Whenever your React testing library test fails, these are 3 amazingly useful functions you now know about to help you debug and fix the broken or failing tests.

Conclusion

In this post to debug React testing library tests, you witnessed how to use screen.debug instead of the usual console.log to easily see the rendered HTML for your react component. You also learned that the screen.debug actually uses prettyDOM helper function in the background. Finally, you became aware of the logRoles helper function that prints all the roles available for a given React component which makes it much easier to query Nodes by roles to write further tests. Keep on writing those amazing tests!

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