Why I love to test with react-testing-library

John Brennan
Mar 16, 2019 · 10 min read

The ReactJS team released the Hooks API in 16.8. In an accompanying blog post, react-testing-library got an official endorsement from them. They highlight the library for how it makes testing easy. I am delighted to see this. I wanted to share why this library is awesome and how it will empower you to be a better frontend developer.

This post will explore some of the philosophy behind why testing is important. The goal is to share some practical tips on how to use react-testing-library that you can use today.

Why test? 🤔

As software engineers, we get to work on interesting domains. In my case, I work over at VSware helping make principals and teachers day to day work easier. It’s amazing! But, I tend to struggle with the fact that there is so much to it. Working on existing features. Fixing bugs and defects. Implementing new requirements. Traversing and understanding legacy codebases. Trying to keep good, clean engineering practices. I find this overwhelming. Complexity is painful. 🤯

Test tubes, something you don’t use when testing code.

It is irrational behaviour. I was getting bogged down by this complexity. But, one way that helped me be less fretful was being serious about my testing disciplines. Testing gives me confidence!

In my career, I’ve been with companies that focus on delivering a product. The need for confidence comes from my experiences with this. They focus on one thing: providing value to their customers. I’m hired to make someone’s life easier. To provide that value. They have a set of things they need to do every day, and it’s important that they’re able to do their job. They do not care about the latest frontend framework, or how many hooks you can use, they want to get their job done. Part of this confidence comes from having a suite of green, passing tests.

I can go home feeling easy having certainty. Certain that critical, value-generating features are working. There’s joy knowing it’s unlikely I’ll get paged at three in the morning because a feature broke.

In turn, this confidence gives great benefits. Being able to keep up momentum for developing new features at a faster rate. A more useful asset to my teammates. Having context when I haven’t touched a feature in months, or ever before.

Having tests is like having a set of low-level documentation. But these documents verify your software and tell you about what it is going to do.

What do I test? 🤷‍♀️

So we know that testing is important, but what do you actually test? It all goes back to your user. What do they need to do in your product? What gives them value? Those critical value generating features and flows are what to focus on. Ensuring the happy paths all work is a great place is start if you are struggling to come up with a solid set of tests. Think of what the user needs to do and try and make your test do that.

How to test 💡

This is where we enter react-testing-library. Written by Kent C. Dodds, this is a library that promotes focusing your tests on user behaviour. It provides a nice set of APIs that help make things easy. Easy to grab elements off the page/component. Also easy to manipulate these elements, ready for assertions with tools like Jest.

I’ll detail some ways you can get the best use out of it. I want to promote some techniques that give tests the same love as the production code.

Make your tests resilient to change 💃

Worthy to note: A lot of these ideas come with full credit by an amazing article by Kent C. Dodds. I’m putting my own spin on this already well-established idea!

Tests shouldn’t break by changing some implementation detail of your component. CSS changes, refactoring to Hooks or moving components around in your code. Things shouldn’t break if the functionality remains the same. Nonetheless, it’s fair to expect things to break if their APIs change. There are very few other cases that justify this.

Let’s take a simple custom <Input /> component. A small wrapper around the native input with an optional label:

export function Input({ label, id, ...props }) {
return (
<>
{label && (
<label
className="myFancyLabelStyles"
htmlFor={id}
>
{label}
</label>
)}
<input
className="myFancyInputStyles"
id={id}
{...props}
/>
</>
);
}

First of all your immediate gut reaction would be like: why would I even bother testing this? It’s too simple. This might be true depending on your use case! Let’s say though, this <Input /> appears all across your product. Forms and user interactions all utilise it. It’s valid to verify it has the standard behaviour of a normal HTML input tag. This is so that there aren’t breakages across your product if something bad gets into this component.

Having a good selector for your component can help make your test resilient. Let's examine the advantages and disadvantages of some:

By class/className eg..myFancyInputStyles

Classes are a concern of styling. I disagree with using them in your test. You should be able to change CSS without your test messing up of wanting to use a different class/set of styles. What if I want to replace my input style with the Tailwind CSS framework? Or styled-components? Avoid using these in your test.

By the HTML tag eg. input 🤷‍♂️

I would also avoid these unless I knew for certain I would only have one input in my component that I’m testing. Otherwise, there could be dancing trying to organise various input elements. Trying to tell them apart from each other would be a mess.

By aria label eg. [aria-label="username"] 👍

A great idea. Promotes accessibility by labelling elements by what their functional role is. In the case of the example, it’s easy to see its an element that concerns itself with a username.

By test ID eg. [data-testid="username"] 🙌

By using a test ID, we are making the relationship between a test and a part of the component explicit. react-testing-library provides some useful helpers to work with components written in this way.

By label/placeholder value eg. /username/i 🤗

To me, this is one of the most intuitive methods and has helped me read and write tests in a human way. Its what has made me completely fall in love with react-testing-library.

Heres some examples of testing our above <Input/> :

import 'react-testing-library/cleanup-after-each'
import React from 'react'
import { Input } from './index'
import { render, fireEvent } from 'react-testing-library'
it('Able to get input by placeholder', () => {
const { getByPlaceholderText } = render(<Input placeholder={'Name'} />)
const input = getByPlaceholderText('Name')
fireEvent.change(input, { target: { value: 'John' } })
expect(input.value).toBe('John')
})
it('Able to get by test id', () => {
const { getByTestId } = render(<Input data-testid="username" />)
const input = getByTestId('username')
fireEvent.change(input, { target: { value: 'johnb' } })
expect(input.value).toBe('johnb')
})
it('Able to get input by label', () => {
const { getByLabelText, debug } = render(
<Input label={'Surname'} id="surname" />
)
const input = getByLabelText(/surname/i)
debug(input)
fireEvent.change(input, { target: { value: 'Brennan' } })
expect(input.value).toBe('Brennan')
})

The power comes from the render function that it provides in its top-level API. It works like the render from react-dom. Except it returns a lot of useful query functions to get parts of the component. getByPlaceHolderText , getByTestId and getByLabelText get our input element. react-testing-library provides another top-level function called fireEvent. This can send an event to a component to then do some assertions on the element.

Another cool feature I used in the last test is debug which is also returned from the render function. Often I find it difficult to debug tests when using Jest. It can be difficult to understand what’s going on in the component without being able to see it in the browser. debug pretty prints your component. It can take an optional parameter to only print the HTML, depending on what element you pass to the function.

The API of the library is small, there’s only a few you need to know. But they all provide you with the tools to write powerful tests. I recommend playing with the library in an existing project of yours and see how it fairs.

Structuring your tests for success 💪

The next part I want to focus on is the idea of getting the best bang for your buck when writing your tests. I’ve wasted a lot of time writing lots of ugly boilerplate, and it deters me from writing tests. It’s annoying to set up. It’s a mindset you have to get into, treating your code with the same level of respect as production code. Avoiding unnecessary duplication where applicable, using patterns to help make your life easier.

The global render function 🌎

There are a lot of global concerns in a typical React application. Global state management with Redux. Routing with something like Reach Router or React Router. Authentication. Internationalization. I could go on. When starting out, I found it confusing trying to juggle these concerns along with my tests. Having to try mock things, ending in ugly boilerplate. It’s frustrating: they’re implementation details and I don’t want to care about them.

Where react-testing-library shines is in the power of the render function. You can wrap the global concerns by extending the functionality of the render function. Let’s look at an example to illustrate this:

import { render as r } from 'react-testing-library'
import { createStore } from 'redux'
import { Provider as ReduxProvider } from 'react-redux'
import { LocationProvider, createHistory } from "@reach/router"
import { AuthenicationProvider, createAuth } from '../auth'
import { reducer } from '../state'
export function reducer(ui, {
initialState = {},
store = createStore(reducer, initialState),
history = createHistory(),
auth = createAuth()
} = {}) {
const WrapperUI = () => (
<ReduxProvider store={store}>
<AuthenicationProvider auth={auth}>
<LocationProvider history={history}>{ui}</LocationProvider
</AuthenicationProvider>
</ReduxProvider>
)
return { ...r(<WrappedUI />), store, history }
}

Here I have redux , @reach/router and a fictitious custom Authentication module. In a test utility file, I’m taking the ‘default’ render function and aliasing it to r. Then I export my own render function which has knowledge of all global dependencies. I’m also using a list of options that have some sane defaults. But, you can override depending on the specific test use-case.

Let’s say you want to test some non-initial step in a user flow. An option is sending some initial information to the redux store. An example where I want to pass in a starting path, so I can do some assertions verifying going to the correct URL. The returned history object would be useful here.

You can extend this render function for whatever makes sense for your application. If it's a global concern — add it here when your tests call for it so that you don’t have to mock as many things.

The test specific render function

I find difficult to understand tests when there is a lot of boilerplate, it makes the test useless to me. It’s mental overhead that I hate and makes testing less appealing. I want to be able to understand what the test is trying to do as quick as possible. I don’t want to get bogged down about the details of the dancing it needs to perform assertions.

When combined with the global render function, test specific render functions shine. You can capture areas of interest in your component. Those used for triggering actions and manipulating data. They are all encapsulated in this function, extending the default utilities provided.

For an example <LoginForm /> component, we could have a structure in a test like so:

import { render as r } from '../test-utils'function render(ui, options) {
const utils = r(ui, options)
return {
...utils,
username: utils.getByLabelText(/username/i),
password: utils.getByLabelText(/password/i),
login: utils.getByText(/login/i),
successModal: () => utils.getByTestId('login-success')
}
}

For this particular component, this would provide all the things I need to write tests. Input getters for the username and password fields. Along with the login button and success message. The successModal is in a function is because I know it won’t render on the initial mount. I can delay the call of execution so the test doesn’t break not being able to find a particular test ID.

Using this in an actual test might look like:

it('Should let me login given a username and password', () => {
const auth = { login: jest.fn() }
const { username, password, login } = render(<Login /> { auth })

fireEvent.change(username, { target: { value: 'john' } })
fireEvent.change(password, { target: { value: 'sekret' } })
fireEvent.click(login)
expect(auth.login).toHaveBeenCalledWith('john', 'sekret')
})

It’s a simple example but it illustrates an important goal. There is no fluff other than the arrangement of the authentication mock to avoid hitting a real API. It is the elements that we need and no more. A lot nicer to read than having to scan through getByX statements in your test code. This test is to the point of what the assertion is trying to achieve.

The call to action 📣

I learned all these ideas from the awesome testingjavascript.com course. I recommend you ask your manager to get this course for you. Or buy it if you’re in a position to do so! It was beneficial for me and helped me ‘click’ with testing UI. You can also head over to testing-library.com to learn more about the library and its amazing, simple APIs.

Conclusion 🔚

The ideas presented in this article aren’t groundbreaking. They pack a punch for a topic I once considered boring for a long time. I can’t recommend you giving this library a shot enough. It’s been super useful to me, where I’ve actually wanted to write tests as part of my feature work in my job. The patterns I’ve discussed have given me wins over and over, where it feels like a dream to work with. It’s given me that confidence I mentioned at the start of the post. I am more comfortable shipping my features to production.

I talked all about the content of this post at the ReactJS Dublin meetup in January 2019. If you’d like a video version of the presented ideas, give it a watch!

If you have any questions or thoughts about testing in the frontend, I would love to hear it! Catch me on twitter https://twitter.com/jgbrenno

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade