React Testing Library Common Scenarios

July 16, 201920 min read

Last reviewed July 16, 2019

tl;dr – React Testing Library Examples

When I was first introduced to React Testing Library, my first reaction was “alright, yet another React framework” and I didn’t pay much attention to it. The library quickly gained traction, but as I had been working with Enzyme for a while, I didn’t bother.

With the introduction of React Hooks, testing with Enzyme became harder. To be frank, I never completely enjoyed Enzyme and paradoxically this was the main reason I relucted to switch libraries – I was too committed. Moreover, most of the test scenarios I use in my workdays were already developed and I needed to do little thinking when creating new tests.

Yet, every now and then I’d struggle writing some test, especially if it involved mocking API calls. Somehow the tests didn’t feel right. Finally, I decided to give React Testing Library a shot and, well, as you might have imagined, I love it!

This article is a compilation of common test scenarios that you are likely to find when developing your application. For each scenario, I describe the problem and how you can write a test for it with Jest + React Testing Library.

Table of Contents

  1. Controlled Component
  2. Input Change
  3. Focused Element
  4. Effects
  5. setTimeout
  6. Fetch
  7. Multiple API calls

If you are reading this post as a reference, you can skip the next session. Else, let us understand what makes RTL better.

Why React Testing Library

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

The above sentence is the guiding principle of React Testing Library and in fact says a lot about it. With Enzyme, tests were always uncomfortably implementation-dependent. They were fragile to changes.

With RTL, when you render a component, you have the actual DOM element exposed to you. This makes your test way more predictable and reduces the learning curve (it boils down to good ol’ JavaScript).

One can argue that all you can do with RTL is possible to be done with Enzyme. True. But in the end, RTL was designed to enforce good practices. That’s why you render and access elements through accessibility-compliant attributes (get via semantic attributes such as text, label and placeholder, instead of using meaningless class selectors).

Another pleasant difference is that tests are smaller, which is a good thing.

Enough with the prelude, let us take a look at our first scenario.

Scenario 1 - Controlled Component

Suppose that you need to create a button component that is meant to be shared across your app:

import React from 'react';

const Button = (props) => {
  return (
    <button onClick={props.onClick}>{props.text}</button>
  );
}

export default Button;

There are two things that you might want to assert:

  1. The button is rendered with the correct text;
  2. Whatever function passed as the onClick prop is called after click.

Here’s how you can write a test to address the first point:

import React from 'react';
import Button from './Button';
import { render, cleanup } from '@testing-library/react';

afterEach(cleanup);

const defaultProps = { 
  onClick: jest.fn(),
  text: "Submit" ,
};

test('button renders with correct text', () => {
  const { queryByText } = render(<Button {...defaultProps} />);
  expect(queryByText("Submit")).toBeTruthy(); 
});

Notice that render returns functions that allow you to select and manipulate DOM elements. In our case, we are using queryByText, which (surprise!) allows you to query nodes by their text. It will return null if no nodes satisfy the query and throw an error if more than one are found (when you might consider using queryAllBy).

So our test was able to assert that there is a DOM node whose text is Submit. And if we want to make sure that it works for other prop values too?

test('button renders with correct text', () => {
  const { queryByText, rerender } = render(<Button {...defaultProps} />);
  expect(queryByText("Submit")).toBeTruthy(); 

  // Change props
  rerender(<Button {...defaultProps} text="Go" />);
  expect(queryByText("Go")).toBeTruthy(); 
});

The rerender function allows you to manually trigger a rerender, this time with a different prop. Our first test is done!

Now let’s check how we can guarantee that by clicking on the button we call the onClick prop:

import React from 'react';
import Button from './Button';
import { render, fireEvent, cleanup } from '@testing-library/react';

...

test('calls correct function on click', () => {
  const onClick = jest.fn();
  const { getByText } = render(
    <Button {...defaultProps} onClick={onClick} />
  );
  fireEvent.click(getByText(defaultProps.text));
  expect(onClick).toHaveBeenCalled();
});

Here we are creating a simple Jest mock function and passing it as the onClick prop to the Button component. We then use getByText to select the button this time (getByText will throw an error if 0 or more than one element match the query).

With the node selected, we then call fireEvent.click, which is the declarative way of RTL firing events. All we need to do next is to confirm that our mock function was indeed called.

And our first basic scenario is complete. The complete test is in GitHub.

Scenario 2 - Input Change

Another very common use-case is an input change that modifies the UI. Consider a text field for the user’s name and a greeting that changes with the input:

import React, { useState } from 'react';

function ChangeInput() {
  const [name, setName] = useState("");
  return (
    <div>
      <span data-testid="change-input-greeting">
        Welcome, {name === "" ? "Anonymous User" : name}!
      </span>
      <br />
      <input 
        type="text" 
        aria-label="user-name" 
        placeholder="Your name"
        value={name}
        onChange={e => setName(e.target.value)}
      />
    </div>
  );
}

export default ChangeInput;

Notice that we are using React Hooks to manage the state. However, internal implementations shouldn’t matter at all. Here’s how the test would look like:

import React from 'react';
import ChangeInput from './ChangeInput';
import { render, fireEvent, cleanup } from '@testing-library/react';

afterEach(cleanup);

test('displays the correct greeting', () => {
  const { getByLabelText, getByTestId } = render(<ChangeInput />);
  const input = getByLabelText("user-name");
  const greeting = getByTestId("change-input-greeting");
  expect(input.value).toBe("");
  expect(greeting.textContent).toBe("Welcome, Anonymous User!")
  fireEvent.change(input, { target: { value: "Rafael" }});
  expect(input.value).toBe("Rafael");
  expect(greeting.textContent).toBe("Welcome, Rafael!");
});

Pretty straightforward, we select the input via the aria-label attribute (a good example of the library enforcing accessibility good practices) and also check that both the greeting and the input have the correct initial value. Then, we call fireEvent.change to trigger a change event, making sure we pass the correct event object with the new input.

Finally, we need to assert that the input has the correct value and the right greeting is being displayed. And there two of my favorite things with RTL stand:

  1. We access values and attributes as we would with regular DOM nodes. So getting an element’s text is as easy as accessing textContent.
  2. Nodes are passed as reference, so if you assign them to a variable before a change, you can use the same variable to later check if any of its attributes have changed. This is a huge plus over Enzyme, where I found myself many times dealing with bugs where I assumed a change would be there but I needed to select the element again. Notice how the same greeting variable has different textContent values based on the event triggered.

Scenario 3 - Focused Element

Accessible applications need to keep track of the focused element. For example, suppose you want to make sure that after clicking on a button, the focus goes to a given input.

import React, { useRef } from 'react';

const FocusInput = () => {
  const inputRef = useRef(null);
  return (
    <div>
      <input 
        type="text" 
        aria-label="focus-input" 
        ref={inputRef}
        placeholder="Focus me!"
      />
      <button onClick={() => inputRef.current.focus()}>
        Click to Focus
      </button>
    </div>