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.
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:
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.
Now that we're all setup, let's create two components to test inside of a components
directory:
<ul>
.First, let's start with the 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
.
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:
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.
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>
);
}
We are going to have three scenarios we want to test:
<li>
elements after fetch.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.
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.