DEV Community

Nikolay Gushchin
Nikolay Gushchin

Posted on

5 Tricks for React Testing Library to make your unit tests better

Writing tests for your React components is just as important as writing the components themselves. React Testing Library (RTL) is one of the most popular tools for testing React applications, and its simplicity and focus on testing user interactions make it a must-have for developers. But are you using it to its full potential? In this article, I’ll share five tricks that will help you get the most out of React Testing Library and make your unit tests more effective and maintainable.


1. Use screen for Queries Instead of Destructured Queries

A common mistake developers make is destructuring queries from render. While this works, it can lead to inconsistent test readability. Instead, use the screen object for all your queries.

Why it helps:

  • Improves test readability by avoiding unnecessary destructuring.
  • Clearly indicates you're interacting with elements rendered on the screen.

Example:

Instead of this:

const { getByText } = render(<Button text="Click me" />);
expect(getByText(/click me/i)).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

Do this:

render(<Button text="Click me" />);
expect(screen.getByText(/click me/i)).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

Using screen makes your tests more consistent and readable, especially when working with larger test files.


2. Prefer findBy for Asynchronous Elements

If your component renders elements asynchronously (e.g., after an API call or a timeout), always use findBy queries instead of getBy. This ensures your tests wait for the element to appear before running assertions.

Why it helps:

  • Prevents flaky tests caused by timing issues.
  • Makes your tests more robust when dealing with asynchronous components.

Example:

// Component fetches and displays a user's name asynchronously
render(<UserProfile />);
const userName = await screen.findByText(/john doe/i);
expect(userName).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

Using findBy ensures that your test waits for the element to appear in the DOM before asserting it.

You can also achieve this using waitFor, however you can't use them together as findBy already is a combination of getBy and waitFor

// Component fetches and displays a user's name asynchronously
render(<UserProfile />);
await waitFor(() => {
  expect(screen.getByText(/john doe/i)).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

3. Use the within Utility for Scoped Queries

Sometimes, you need to target elements inside a specific container. Instead of writing complex selectors, use the within utility to scope your queries to a specific part of the DOM.

Why it helps:

  • Avoids global queries that might match unintended elements.
  • Makes your tests more precise and focused.

Example:

render(
  <form>
    <fieldset name="Personal Information">
      <legend>Personal Information</legend>

      <label for="personal-name">Name</label>
      <input type="text" name="personal-name" id="personal-name" />

      <label for="personal-email">Email</label>
      <input type="text" name="personal-email" id="personal-email" />
    </fieldset>
    <button type="submit">Submit</button>
  </form>
);

const personalInfoSection = screen.getByRole('group', {
  name: 'Personal Information',
});

// Scoped queries within 'Personal Information'
const personalNameInput = within(personalInfoSection).getByLabelText('Name');
const personalEmailInput = within(personalInfoSection).getByLabelText('Email');

// Fill out the form
userEvent.type(personalNameInput, 'John Doe');
userEvent.type(personalEmailInput, 'john@example.com');

// Assert that the inputs have the correct values
expect(personalNameInput).toHaveValue('John Doe');
expect(personalEmailInput).toHaveValue('john@example.com');
Enter fullscreen mode Exit fullscreen mode

This approach helps you write cleaner, context-specific tests.


4. Leverage userEvent for Simulating User Interactions

While React Testing Library provides utilities like fireEvent, the userEvent library offers a more realistic way to simulate user interactions. It mimics how real users interact with your app, including typing, clicking, and tabbing.

Why it helps:

  • Simulates events more realistically.
  • Handles more complex interactions like typing or pasting text.

Example:

import userEvent from '@testing-library/user-event';

render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /submit/i });

await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(submitButton);

expect(screen.getByText(/welcome/i)).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

Using userEvent ensures your tests reflect real-world interactions more accurately.


5. Utilize debug() to Inspect the DOM

Struggling to figure out why your test is failing? Use RTL’s debug() method to print the current state of the DOM to your console. This is especially useful when working with complex components.

Why it helps:

  • Quickly identifies why elements are missing or tests are failing.
  • Makes troubleshooting faster and easier.

Example:

render(<MyComponent />);
screen.debug(); // Logs the current DOM structure to the console
Enter fullscreen mode Exit fullscreen mode

You can also pass specific elements to debug():

const section = screen.getByRole('region');
within(section).debug();
Enter fullscreen mode Exit fullscreen mode

This is a powerful tool for debugging and understanding what’s happening in your tests.


Bonus Tips:

  • Avoid Testing Implementation Details: Focus on testing what the user sees and interacts with, not internal component states.
  • Combine Matchers for Specificity: Use matchers like .toHaveTextContent() or .toHaveAttribute() for detailed assertions.
  • Clean Up After Tests: While React Testing Library does this automatically, calling cleanup() explicitly in afterEach ensures no DOM leaks.
  • Not necessarily related to RTL but I prefer using Jest along with Jest plugin which allows to run specific tests and shows coverage directly in IDE

Conclusion

React Testing Library is all about writing tests that reflect how users interact with your application. By applying these five tricks, you can write cleaner, more reliable, and maintainable tests. Whether it’s leveraging userEvent for realistic interactions or using within for scoped queries, these tips will help you elevate your testing game. Start implementing them in your next project and see the difference for yourself! 🎯

Happy testing! 🎉

Billboard image

Use Playwright to test. Use Playwright to monitor.

Join Vercel, CrowdStrike, and thousands of other teams that run end-to-end monitors on Checkly's programmable monitoring platform.

Get started now!

Top comments (0)

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay