Testing React With React Testing Library

05/01/22

Intro

Testing your React applications is of utmost importance nowadays. With so much interactivity taking place on the client side, it could be very easy to make a mistake and break your application. That's where React Testing Library can come in and help. Today, I'll be going over two scenarios that you might come across in the real world, and how you can test them.

Setting Up React Testing Library

For this tutorial, we will use create-react-app in order to make things easy to follow. In a directory of my choice, I am going to create a new app via:

- npx create-react-app testing-practice
- cd testing-practice

Once in the testing-practice root directory, I am going to install the following packages with yarn:

  • @testing-library/jest-dom
  • @testing-library/react
  • @testing-library/user-event
  • axios

Next, in my package.json, I am going to add a little setup for our tests.

"setupFilesAfterEnv": ["./src/setupTests.js"]

In order for React Testing Library to work, we need to create that setupTests.js file in ./src. Here's what we want to put inside of it:

import "@testing-library/jest-dom/extend-expect";

If that file already exists because create-react-app created it, remove what's inside of it and paste in what I have above.

Now that we have that in place, let's do one more thing before we start building out some components to test. Let's create a __tests__ directory inside of ./src. We will come back to this directory once we are ready to start writing our tests.

Creating Components To Test With React Testing Library

Now that we're all setup, let's create two components to test inside of a components directory:

  • A form that accepts some user input, and executes a callback on submit.
  • A component that fetches a list of pizza flavors from an API, and renders each of them inside a <ul>.

First, let's start with the form component.

Building Our Form Component

In order to stay focused on learning React Testing Library, I will not be styling anything with CSS. Here is what our form component looks like:

import { useState } from 'react';

export default function Form({ onSubmit }) {
  const [form, setForm] = useState({ name: '', email: '' });

  const handleChange = (e) => {
    const { value, name } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onSubmit(form);
      }}
    >
      <label htmlFor="name">Full name</label>

      <input
        id="name"
        name="name"
        type="text"
        placeholder="Your name"
        value={form.name}
        onChange={handleChange}
      />
      <br />

      <label htmlFor="email">Email</label>

      <input
        id="email"
        type="text"
        name="email"
        placeholder="xyz@gmail.com"
        value={form.email}
        onChange={handleChange}
      />
      <br />

      <br />
      <button type="submit">Submit</button>
    </form>
  );
}

Our form is pretty simple. We have two controlled inputs that accept a name and an email address respectively. When the user presses the submit button, it will execute the onSubmit callback that is passed as a prop. Notice that we pass the forms values as the first argument of onSubmit.

Testing Our Form Component

The next thing we need to do is create a Form.test.js file inside of __tests__. From here, let's setup a basic test skeleton.

import React from 'react';
import Form from '../components/Form';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

  test('it renders the form', () => {
    render(<Form />);
  });

  test('it accepts input properly', async () => {
    render(<Form />);
  });

  test('it passes the forms values on submit', async () => {
    const onSubmit = jest.fn();
    render(<Form onSubmit={onSubmit} />);
  });

We want to test these three scenarios:

  • The form is rendered properly on initial mount.
  • Each input accepts user input properly.
  • The onSubmit callback is called when the form is submitted.

Let's fill in the details for the first test.

test('it renders the form', () => {
  render(<Form />);

  expect(screen.getByLabelText('Full name')).toBeInTheDocument();
  expect(screen.getByPlaceholderText('Your name')).toBeInTheDocument();
  expect(screen.getByLabelText('Email')).toBeInTheDocument();
  expect(screen.getByPlaceholderText('xyz@gmail.com')).toBeInTheDocument();
});

Pretty simple, right? Let's jump to the second test where we will simulate filling out the form.

test('it accepts input properly', async () => {
  render(<Form />);

  const name = screen.getByPlaceholderText('Your name');
  await userEvent.type(name, 'Dana Scully');
  expect(name).toHaveValue('Dana Scully');

  const email = screen.getByPlaceholderText('xyz@gmail.com');
  await userEvent.type(email, 'd.scully@fbi.gov');
  expect(email).toHaveValue('d.scully@fbi.gov');
});

Notice how we had to explicitly mark this test as async? This is because userEvent.type returns a Promise. Finally, let's create our last test, testing the callback function.

test('it passes the forms values on submit', async () => {
  const onSubmit = jest.fn();
  render(<Form onSubmit={onSubmit} />);

  const name = screen.getByPlaceholderText('Your name');
  await userEvent.type(name, 'Dana Scully');

  const email = screen.getByPlaceholderText('xyz@gmail.com');
  await userEvent.type(email, 'd.scully@fbi.gov');

  fireEvent.click(screen.getByText('Submit'));

  expect(onSubmit).toHaveBeenCalledWith({
    name: 'Dana Scully',
    email: 'd.scully@fbi.gov',
  });
});

We create onSubmit as a mock Jest function so that we can check when it's been executed, and what arguments it's been executed with. Notice how we make use of fireEvent.click on the submit button.

Testing Async Behavior With React Testing Library

Now that we are finished with our form, we can create our component that fetches a list of pizza flavors from our "API". Here's what that component looks like:

import axios from 'axios';
import { useEffect, useState } from 'react';

export default function Pizzas() {
  const [pizzas, setPizzas] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  const fetchPizzas = async () => {
    try {
      const res = await axios.get(
        'https://gist.githubusercontent.com/DZuz14/fd1c7e17f5929dc12f3d0406ce5888b2/raw/1910b3ce56284cc0eeb9c90b8961f6a468aa3dec/pizzas.json'
      );
      setPizzas(res.data);
      setLoading(false);
    } catch (error) {
      setError(true);
    }
  };

  useEffect(() => {
    fetchPizzas();
  }, []);

  return (
    <div>
      {loading && <div>Loading...</div>}

      {error && <p>Failed to fetch pizzas.</p>}

      <ul>
        {pizzas.map((p) => (
          <li key={p}>{p}</li>
        ))}
      </ul>
    </div>
  );
}

Creating A Test Skeleton

We are going to have three scenarios we want to test:

  • If the loading indicator is visible and no pizza's are visible on initial mount.
  • That the list of pizzas are rendered as <li> elements after fetch.
  • An error is shown if fetching the pizza's fails.
import React from 'react';
import Pizzas from '../components/Pizzas';
import { render, screen } from '@testing-library/react';
import axios from 'axios';

jest.mock('axios');

const pizzaTypes = ['Cheese', 'Pepperoni', 'Pineapple'];

describe('Pizzas', () => {
  test('it shows a loading indicator initially, and no pizzas', async () => {
    //
  });

  test('it displays a list of pizzas', async () => {
    //
  });

  test('it shows an error when fetching pizzas fails', async () => {
    //
  });
});

Seeing as we are using axios to make the network request, we need to mock it so that we don't send the real request. Now that we have the basics setup, let's fill in the first test.

test('it shows a loading indicator initially, and no pizzas', async () => {
  axios.get.mockImplementation(() => Promise.resolve({ data: pizzaTypes }));
  render(<Pizzas />);

  expect(await screen.findByText('Loading...')).toBeInTheDocument();
  expect(screen.queryAllByRole('listitem')).toEqual([]);
});

For this test, and the other's, we make a mock for the axios.get method, and make it resolve a Promise that gives us a list of pizza flavors. This will prevent us from hitting the real API. Also, notice how we query for a list items and expect it to be an empty array. This is because on initial render we haven't hit the API yet, and shouldn't have any list items.

Let's fill in the second test now. We will be testing to see if our list items are in the DOM after the mocked network request succeeds.

test('it displays a list of pizzas', async () => {
  axios.get.mockImplementation(() => Promise.resolve({ data: pizzaTypes }));
  render(<Pizzas />);

  const listItems = await screen.findAllByRole('listitem');
  expect(listItems).toHaveLength(3);
});

Seeing that our mock response data consists of ['Cheese', 'Pepperoni', 'Pineapple'], there should be three list items created.

Now for our final test, seeing if our component responds to errors properly.

test('it shows an error when fetching pizzas fails', async () => {
  axios.get.mockImplementation(() => Promise.reject(Error()));
  render(<Pizzas />);

  expect(
    await screen.findByText('Failed to fetch pizzas.')
  ).toBeInTheDocument();
});

This time we change the mock of axios.get to return a rejected Promise instead. This should cause our component to render some text that notifies the user that the request failed.

Wrapping Up Testing With React Testing Library

That's about it for testing React components with React Testing Library for now folks! As always, if you have any problems understanding any of this, feel free to shoot me a message, and I'll make sure it's crystal clear.

P.S. Here is a full working version on Github.

Want To Level Up Your JavaScript Game?

Book a private session with me, and you will be writing slick JavaScript/React code in no time.