Fix Your Failing Tests: A Debugging Checklist for React Testing Library
Solutions to the most common React testing issues
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.
Unable to fire a ${event.type} event - please provide a DOM element.
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);
});
Pro Tip
If your code looks like this:
getUser(userId).then((user) => setUser(user));
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)
);
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();
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
});
Wrap your assertion in a waitFor
to allow Promise.all
to resolve.
await waitFor(() => expect(myElement).toBeInTheDocument());
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'));
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
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();
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();
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()
);
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 await
ed 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!
});
One way to solve this is using a beforeEach
:
let user;
beforeEach(() => {
user = {
userName: 'user1'
};
});
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' } });
// ...
});
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>
In your test, you have to render the component with the same path:
render(
<MemoryRouter>
<Route path={['/users', '/users/:companyId']}>
<UserScreen />
</Route>
</MemoryRouter>
);
Let's say you forget and only do part of the path:
render(
<MemoryRouter>
<Route path="/users">
<UserScreen />
</Route>
</MemoryRouter>
);
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
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.