Getting Started with React Testing Library

Avatar of Kingsley Silas
Kingsley Silas on

I can guess what you are thinking: another React testing library? So many have already been covered here on CSS-Tricks (heck, I’ve already posted one covering Jest and Enzyme) so aren’t there already enough options to go around?

But react-testing-library is not just another testing library. It’s a testing library, yes, but one that’s built with one fundamental principle that separates it from the rest.

The more your tests resemble the way your software is used, the more confidence they can give you.

It tries to address tests for how a user will use your application. In fact, it’s done in such a way that tests won’t break even when you refactor components. And I know that’s something we’ve all run into at some point in our React journey.

We’re going to spend some time writing tests together using react-testing-library for a light to-do application I built. You can clone the repo locally:

git clone https://github.com/kinsomicrote/todoapp-test.git

And, if you do that, install the required packages next:

## yarn
yarn add --dev react-testing-library jest-dom

## npm
npm install --save-dev react-testing-library jest-dom

In case you’re wondering why Jest is in there, we’re using it for assertion. Create a folder called __test__ inside the src directory and create a new file called App.test.js.

Taking snapshots

Snapshot tests keep a record of tests that have been performed on a tested component as a way to visually see what’s changes between changes.

When we first run this test, we take the first snapshot of how the component looks. As such, the first test is bound to pass because, well, there’s no other snapshot to compare it to that would indicate something failed. It only fails when we make a new change to the component by adding a new element, class, component, or text. Adding something that was not there when the snapshot was either created or last updated.

The snapshot test will be the first test we will be writing here. Let’s open the App.test.js file and make it look like this:

import React from 'react';
import { render, cleanup } from "react-testing-library";
import "jest-dom/extend-expect";
import App from './App';

afterEach(cleanup);

it("matches snapshot", () => {
  const { asFragment } = render(<App />);
  expect(asFragment()).toMatchSnapshot();
});

This imports the necessary packages we are using to write and run the tests. render is used to display the component we want to test. We make use of cleanup to clear things out after each test runs — as you can see with the afterEach(cleanup) line.

Using asFragment, we get a DocumentFragment of the rendered component. Then we expect it to match the snapshot that had been created.

Let’s run the test to see what happens:

## yarn
yarn test

## npm
npm test

As we now know, a snapshot of the component gets created in a new folder called __snapshots__ inside the __tests__ directory if this is our first test. We actually get a file called App.test.js.snap in there that will look like this:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`matches snapshot 1`] = `
<DocumentFragment>
  <div
    class="container"
  >
    <div
      class="row"
    >
      <div
        class="col-md-6"
      >
        <h2>
          Add Todo
        </h2>
      </div>
    </div>
    <form>
      <div
        class="row"
      >
        <div
          class="col-md-6"
        >
          <input
            class="form-control"
            data-testid="todo-input"
            placeholder="Enter a task"
            type="text"
            value=""
          />
        </div>
      </div>
      <div
        class="row"
      >
        <div
          class="col-md-6"
        >
          <button
            class="btn btn-primary"
            data-testid="add-task"
            type="submit"
          >
            Add Task
          </button>
        </div>
      </div>
    </form>
    <div
      class="row todo-list"
    >
      <div
        class="col-md-6"
      >
        <h3>
          Lists
        </h3>
        <ul
          data-testid="todos-ul"
        >
          <li>
            <div>
              Buy Milk
              <button
                class="btn btn-danger"
              >
                X
              </button>
            </div>
          </li>
          <li>
            <div>
              Write tutorial
              <button
                class="btn btn-danger"
              >
                X
              </button>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</DocumentFragment>
`;

Now, let’s Test DOM elements and events

Our app includes two to-do items that display by default the first time the app runs. We want to make sure that they do, in fact, show up by default on the first app run so, to test this, we have to target the unordered list (<ul>) and check the length. We expect the length to be equal to two — the number of items.

it('it displays default todo items', () => {
  const { getByTestId } = render(<App />);
  const todoList = getByTestId('todos-ul');
  expect(todoList.children.length).toBe(2);  
});

We’re making use of getByTestId in that snippet to extract the test IDs from the App component. We then set todoList to target the todos-ul element. That’s what should return as two.

Using what we’ve learned so far, see if you can write a test to assert that a user can enter values in the input field. Here are the things you’ll want to do:

  • Get the input field
  • Set a value for the input field
  • Trigger a change event
  • Assert that the input field has its value as the one you set for it in Step 2

Don’t peek at my answer below! Take as much time as you need.

Still going? Great! I’ll go grab some coffee and be right back.

Mmm, coffee. ☕️

Oh, you’re done! You rock. Let’s compare answers. Mine looks like this:

it('allows input', () => {
  const {getByTestId } = render(<App />)
  let item = 'Learn React'
  const todoInputElement = getByTestId('todo-input');
  todoInputElement.value = item;
  fireEvent.change(todoInputElement);
  expect(todoInputElement.value).toBe('Learn React')
});

Using getByTestId, I am able to extract the test IDs in the application. Then I create a variable which is set to the string Learn React, and make it the value of the input field. Next, I obtain the input field using its test ID and fire the change event after setting the value of the input field. With that done, I assert that the value of the input field is indeed Learn React.

Does that check out with your answer? Leave a comment if you have another way of going about it!

Next, let’s test that we can add a new to-do item. We’ll need to get the input field, the button for adding new items and the unordered list because those are all of the elements needed to create an new item.

We set a value for the input field and then trigger a button click to add the task. We’re able to do this by obtaining the button using getByText — by triggering a click event on the DOM element with the text Add Task, we should be able to add a new to-do item.

Let’s assert that the number of children (list items) in unordered list element is equal to three. This assumes that the default tasks are still in tact.

it('adds a new todo item', () => {
  const { getByText, getByTestId } = render(<App />);
  const todoInputElement = getByTestId('todo-input');
  const todoList = getByTestId('todos-ul');
  todoInputElement.value = 'Learn React';
  fireEvent.change(todoInputElement);
  fireEvent.click(getByText('Add Task'))
  expect(todoList.children.length).toBe(3); 
});

Pretty nice, right?

This is just one way to test in React

You can try react-testing-library in your next React application. The documentation in the repo is super thorough and — like most tools — the best place to start. Kent C. Dodds built it and has a full course on testing over at Frontend Masters (subscription required) that also covers the ins and outs of react-testing-library.

That said, this is just one testing resource for React. There are others, of course, but hopefully this is one you’re interested in trying out now that you’ve seen a bit of it but use what’s best for your project, of course.