Better testing with React Testing Library

Tech
5
minute read

If you, like us, enjoy using React for your JavaScript projects, then you probably already use React Testing Library (RTL). You do not? Then it is time you start, because this lightweight solution makes testing easier and faster - the perfect tool in your React toolkit.

Let’s start at the beginning: Why do we test?

For many junior developers, testing can feel like an unnecessary delay - instead of implementing the code they have worked so hard on, time has to be spent running tests and possibly correcting entire sections of code. We are not exempt from that, and many of us are familiar with that feeling of reluctance - but we all learned one way or another that there is no way around testing.

In fact, unit tests are an integral part of the definition of done and simply have to be written.

Raul Bodrea recounts a past project, where, “We always advocated for writing tests but come sprint planning, we found we were too busy implementing features to find time for writing unit tests. It didn’t take long for that project to grow and soon enough we were spending time playing whack a mole - squashing one bug just to spot another.”

Most developers have a similar experience and sooner or later come to the conclusion that spending time writing unit tests makes for faster deliveries and more dependable software. We test because testing ensures the code we write is future-proof, generating fewer legacy issues, running more reliably and integrating more smoothly with later additions and dependencies.

RTL is born

Back in the olden days, developers were using Enzyme to test React apps. This allowed for tests that validate things like component props, HTML element’ attributes and other implementation details that the end user would be blind to. Most web app users are only aware of what is displayed in their browser, and as long as all of the expected components are featured and functional, they are happy.

But developers would often run tests validating information having to do with implementation details rather than actual functionality.

With this in mind, engineer Kent C Dodds came up with a library that, at its core, facilitates testing UI components the same way a normal user would use them. React Testing Library was born. It should be noted that RTL is actually not an original React product - but it has the community’s official seal of approval and is recommended on the official React website.

Features

RTL provides a set of tools that allow developers to find elements and interact with them as a normal user would in a browser, but in a testing environment.

Mounting components

First of all, you will need to set up your unit test, and RTL allows you to do that using the render method. All you need to do is to `import {render} from '@testing-library/react'` and do `const screen = render(<Greeting />)`. As a result, the `<Greeting/>` component will be appended to `document.body`.

You can also pass a second, options parameter to your render call. Among other things, this parameter can help you test components that rely on context providers. In order to do that, you just need to pass the context provider as a `wrapper` in the second parameter. You can find more information on what other options are available here.

Returning to `const screen = render(<Greeting />)`, we have saved the return value of `render` in a constant called `screen`. This is because the `render` function returns:

  • a `container` that allows you to inspect what has been rendered using functions like `document.querySelector`
  • a `rerender` function you can use to test how the component reacts to prop changes
  • an `unmount` function used to test what happens when the component is removed from the page and whether cleanup happens as expected
  • all of the queries described in the next section

You can read more about the `render` function’s return value here.

Finding elements

One of the most important prerequisites for front-end component testing is being able to select UI elements. RTL offers the following query sets that allow you to do just that:

  • A button or a text element:  Use the `ByText()` queries like a user would
  • A form field: The `ByLabelText()` queries help with that. If a11y is not a concern and the form field has no label but a placeholder, you can use `ByPlaceholderText()` to find said field (you should probably also raise an issue on your tracker at this point since fields without labels are not cool)
  • Which form field has a certain value: Use the `ByDisplayValue()` queries. This is especially useful when you want to test how the state of your form fields is affected by certain user interactions
  • An image: If you follow best practice and add alt tags to your images, RTL allows you to look them up using the ‘ByAltText()’ command. If you forgot to add alt text to an image this kind of test would give you an early warning about the missing information
  • SVGs: As long as they have a title, you can run a `ByTitle()` query to locate these files
  • A random div with no clear purpose: add ‘data-testid’ onto it and run a ‘ByTestId’ query to find it. Only use this when none of the other queries work

How about writing a11y related tests? You can run `ByRole()` queries that allow you to find elements by aria role.

Interacting with elements

Now that you have access to a component, the next step is to actually interact with it. RTL’s companion library, `user-event`, allows you to do just that. All you need to do is to initialise a user by running `const user = userEvent.setup()`.

You can then either use low level events such as `keyboard` or `pointer`, which allow you to simulate various keyboard / pointer interactions with a great deal of control, or use the out of the box helpers such as click, which encompasses a pointer move and a pointer button press event in one command.

Here are a few events you might find helpful:

  • Move mouse to an element and click on it: `user.click(element)`
  • Type into a form field: `user.type(element, ‘some password’)`
  • Clear the value from a form field: `user.clear(element)`

There are a lot of other events for you to discover here.

Benefits of using RTL

Besides helping developers better understand how the UI will be used and what potential flaws the DOM might hide, testing with RTL also prevents needless implementation details from sneaking into the unit/integration tests.

Fewer implementation details in tests translates to more flexibility when it comes to the actual implementation, making the tests much more maintainable and focused on the actual functionality rather than how that functionality is built.

In other words, if a test doesn’t care that a `<p/>` has class “foo” but that it displays “bar”, one could update the structure of the page to have “bar” inside of a `<span/>` and the test would still pass.

Writing and running tests using RTL results in more concise unit tests that are easier to manage and maintain. This is important because, often, fixing tests takes longer than actually writing them in the first place. And we all know that time is money (and sanity).

Working with other frameworks? Following the success and popularity of RTL, other libraries such as Angular Testing Library and Vue Testing Library have been developed to help developers optimise testing.

Have a project in mind? Let's talk.

Contact us