DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Fix Your Failing Tests: A Debugging Checklist for React Testing Library
Derek N. Davis
Derek N. Davis

Posted on • Originally published at derekndavis.com

Fix Your Failing Tests: A Debugging Checklist for React Testing Library

When you get stuck fixing your React Testing Library tests, it's hard to remember all the tips and tricks for every issue, and you don't always have a coworker available to help get back on track. But typically, all it takes is asking the right questions.

Today, I'm going to be your coworker. Let's fix those tests.

Making this Process Quicker

Before we get started, I would recommend taking a couple minutes (literally) to readΒ  3 Steps to Frictionless TDD with Jest and VS Code. It will make the debugging process go much smoother, and you'll be happy you did.

And with that, let's begin.

Can't Find My Element

Not being able to find an element is generally a symptom of something else, but it is the most common problem you'll run into. You might be seeing one of these errors:

Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Enter fullscreen mode Exit fullscreen mode
Unable to fire a ${event.type} event - please provide a DOM element.
Enter fullscreen mode Exit fullscreen mode

The most important thing to figure out are the conditions that determine when the element is rendered and go through them one by one.

A Query Typo

Starting with the most common issue, verify that your query (getByText, getByRole, getByPlaceholderText, getByTitle, getByTestId) matches the attributes you're targeting on the element. Copy and paste the correct text to make sure a typo isn't what's causing the issue.

API Mocking

  • Are you missing an API call that should be mocked?
  • Did you mock your API call with the wrong data?
  • Does your API response not meet the conditions to render that element?

For verifying API responses, console.log() is your friend.

getUser(userId).then((user) => {
  // verify your API call is getting the correct response
  console.log('getUser ', user);

  setUser(user);
});
Enter fullscreen mode Exit fullscreen mode

Pro Tip

If your code looks like this:

getUser(userId).then((user) => setUser(user));
Enter fullscreen mode Exit fullscreen mode

You don't have to add curly braces to fit in your console.log(). You can do this little trick to save some time:

getUser(userId).then((user) => 
  console.log(user) || setUser(user)
);
Enter fullscreen mode Exit fullscreen mode

setTimeout and setInterval

If your code is using a setTimeout or setInterval and the callback for it plays a part in making your element show up, save yourself the headache, and put this line at the top of your test file:

jest.useFakeTimers();
Enter fullscreen mode Exit fullscreen mode

Now your test doesn't have to wait on real time to elapse.

Read more about the timer mocks in the Jest Docs.

Using Promise.all? waitFor it... waitFor it...

Another issue you might run into with elements not showing up is with Promise.all. Say your code looks like this:

Promise.all([
    getUser(userId),
    getUserPermissions(userId)
]).then(([user, permissions]) => {
    // set state to make `myElement` show up
});
Enter fullscreen mode Exit fullscreen mode

Wrap your assertion in a waitFor to allow Promise.all to resolve.

await waitFor(() => expect(myElement).toBeInTheDocument());
Enter fullscreen mode Exit fullscreen mode

This would also apply to using other Promise methods like Promise.allSettled or Promise.race.

screen.debug() Your Queries

When your query can't find a particular element, you need to see what React Testing Library is seeing, and screen.debug() is your window into that. If you have a small component, calling screen.debug() without any parameters will be sufficient. But if your component is really big, the output will be truncated, and that doesn't help very much.

Instead, it's better to narrow down what you're looking for. You can put a temporary data-testid on the container of the element you're targeting, and print that out. Now you won't have to sift through 7000 lines of HTML in a terminal.

screen.debug(screen.getByTestId('tempContainerId'));
Enter fullscreen mode Exit fullscreen mode

If you really want to see more than 7000 lines of output, which is the default, it can be changed like this:

DEBUG_PRINT_LIMIT=10000 npm test
Enter fullscreen mode Exit fullscreen mode

Element Is Not Supposed to be There, But It Is

Sometimes you need to ensure that an element is no longer in the DOM, but your test isn't cooperating. Here are a couple of things to try to get in the green again.

Stale Query

One problem you may have in verifying the element is gone is a stale query. Here's the setup:

const hideNameButton = screen.getByText('Hide Name');
const name = screen.queryByText('Derek');

// name should be there
expect(name).not.toBeNull();

// hide it
fireEvent.click(hideNameButton);

// name should not be there.. but it still is :(
expect(name).toBeNull();
Enter fullscreen mode Exit fullscreen mode

In the final assertion, name isn't re-queried. It's stale.

For most test suites, I recommend the solution I discuss in my article on targeting conditional elements. But for a quick fix, you can also inline the queries:

// name should be there
expect(screen.queryByText('Derek')).not.toBeNull();

// hide it
fireEvent.click(hideNameButton);

// name should not be there
expect(screen.queryByText('Derek')).toBeNull();
Enter fullscreen mode Exit fullscreen mode

waitForElementToBeRemoved

Another way of solving this problem is the waitForElementToBeRemoved function. This is more useful in cases where the element may not be removed immediately after some action. Maybe it makes an API call and the promise callback is what removes it. In that case, you could do this:

// name should be there
expect(screen.queryByText('Derek')).not.toBeNull();

// delete the person
fireEvent.click(deletePersonButton);

// name should not be there
await waitForElementToBeRemoved(() => 
  expect(screen.queryByText('Derek')).toBeNull()
);
Enter fullscreen mode Exit fullscreen mode

My Test Passes When Ran by Itself, But Fails When Ran with Other Tests

One of the most frustrating situations is when a test passes by itself, but as soon as you run the whole suite, it fails. Here are a few things to check to solve that problem.

Are You Forgetting an async Somewhere?

Probably the most common cause of tests that fail when ran together is a missing async. When a test runs an operation that needs to be awaited but doesn't have one, it's effectively running that code after the test has completed. This can potentially wreak havoc on the next test, causing it to fail.

To make sure you're not missing an async with React Testing Library functions, you can use eslint-plugin-testing-library . This will warn you if you're using async unnecessarily or you're missing it entirely.

As for your own functions that you're calling from your test, you'll just have to look over them carefully to make sure you're not missing the async keyword.

Do You Have Global Variables in Your Test Suite?

If you're mutating global variables in your test suite, it could lead to some strange issues when running all the tests together.

let user = {
  userName: 'user1'
};

it('should do something', () => {
  // mutating a global variable
  user.userName = 'user2';

  // ...
});

it('should do something else', () => {
  // user.userName is now 'user2' for this test. whoops!
});
Enter fullscreen mode Exit fullscreen mode

One way to solve this is using a beforeEach:

let user;

beforeEach(() => {
  user = {
    userName: 'user1'
  };
});
Enter fullscreen mode Exit fullscreen mode

But a better way is to use a test render function:

function renderUser({ user }) {
  render(<User user={user} />);

  return {
    // ... information and controls in the User component ...
    saveButton: screen.getByText('Save')
  };
}

it('should ...', () => {
  const { saveButton } = renderUser({ user: { userName: 'user1' } });

  // ...
});
Enter fullscreen mode Exit fullscreen mode

This pattern completely removes the question of "did I forget to reset my variables in beforeEach?"

Is Your Component Mutating Global Data?

It's also possible that your component is mutating global variables. Maybe there's data that is set in localStorage, sessionStorage, or (heaven forbid) on the window object during the run of one of your tests. If the next test is expecting to work with a clean copy of those storage mechanisms, that can cause a problem.

Make sure you're resetting those variables in your test render function or beforeEach.

My react-router Params Are Undefined

When you're testing a component directly that is rendered under a react-router Route component in your app, you've got to make sure the path is the same in both contexts. For instance, say you have this in your app:

<Route path={['/users', '/users/:companyId']}>
  <UserScreen />
</Route>
Enter fullscreen mode Exit fullscreen mode

In your test, you have to render the component with the same path:

render(
  <MemoryRouter>
    <Route path={['/users', '/users/:companyId']}>
      <UserScreen />
    </Route>
  </MemoryRouter>
);
Enter fullscreen mode Exit fullscreen mode

Let's say you forget and only do part of the path:

render(
  <MemoryRouter>
    <Route path="/users">
      <UserScreen />
    </Route>
  </MemoryRouter>
);
Enter fullscreen mode Exit fullscreen mode

Then when you try to access companyId from useParams, it will be undefined because it was never declared in the route definition.

const { companyId } = useParams();

console.log(companyId); // undefined
Enter fullscreen mode Exit fullscreen mode

So if your route parameters aren't changing after clicking links or doing a history.push in your test, the first thing to check is the path.

Summary

  • Testing can become really frustrating when you get stuck debugging a long list of failing tests.
  • Use this checklist to get back in the green again.

Hey! If this helped you fix a failing test, please share!

If you've got suggestions for other fixes to common testing scenarios, let me know, so it can help others.

Top comments (0)

🌚 Life is too short to browse without dark mode