Testing React.js Apps
Automated testing is an important yet often overlooked or skipped part of development. It's a shame because adding unit testing to React apps is easy!
Overview & Setup
Testing may not matter much when you are working on a small app but it becomes very important in large scale apps which are worked on by many developers.
Even a small change has the potential to break something in your app and if we have tests written for different features of our app, we can easily detect when a change breaks something in our app - before we ship our code to production.
In this tutorial, you will learn how to test React components using the React Testing Library. Some familiarity with Jest testing framework is assumed.
There also is a popular library named Enzyme, created by Airbnb, which is used for testing React components.
React Testing Library is an alternative to Enzyme. While Enzyme lets us test the implementation details of React components, React Testing Library helps us test the behavior of our React components from the perspective of the users that will use our app.
React Testing Library is used with a testing framework. Although React Testing Library is not dependent on any specific testing framework and can be used with any framework, the official docs of this library recommend to use the library with Jest. In this tutorial, we will use Jest as our testing framework.
If you are using create-react-app for setting up a React project, then you are all set for writing tests for your React components. If you are using a custom React setup, then you need to install React Testing Library. You can install it using the following command
npm install --save-dev @testing-library/react
I recommend that you create a React project using create-react-app
to follow along with this tutorial.
Writing our First Test
Before we begin our testing, we need to have a React component that we can test. Lets create a simple React component.
import React from 'react';function App() {return (<div><h1>Hello World</h1></div>);}export default App;
We will write our tests in a separate file, named App.test.js
.
Notice the *.test.js
in the file name. Jest will automatically detect any files with the name ending with test.js
.
So lets write our first test. In our first test, we will test whether the h1
element in our App
component is rendered in the DOM or not.
import React from 'react';import { render, screen } from '@testing-library/react';import App from './App';test('render h1 element', () => {render(<App />);expect(screen.getByText('Hello World')).toBeInTheDocument();});
Lets understand the code of our first test.
At the top, we have import
statements. First we import React, then we import two main things from React Testing Library:
A
render
function that will be used to render the component which we will be testingAnd a
screen
object that contains different methods to select elements in the document.
We also import the App
component which we want to test.
After that, we are calling the test
function which is a function provided by Jest. It takes two arguments:
A string that represents the name of the test
And a function which contains the code of our test
Inside this function, passed as a second argument to test
function, we first render our App
component using the render
function from React Testing Library. The render
function takes any JSX that you want to render.
The screen
object contains different query methods which the React Testing Library provides. In our first test we used the getByText
query method to select the h1
element using the text it contains. Using an assertion provided by Jest, we test if the h1
element is rendered in the DOM
.
To run our first test, use one of the following commands:
npm test
or
npm run test
If you didn't use create-react-app
to create a React project, you will need to have a test script in the package.json
file before you run the test command.
When you run the test command, you will see the output in the terminal that will indicate whether our test has passed or failed.
To see the HTML output of your component, you could use a method named debug
of the screen
object.
Lets make a slight modification in our first test
test('render h1 element', () => {render(<App />);screen.debug();expect(screen.getByText('Hello World')).toBeInTheDocument();});
If you run Jest in watch mode, it will automatically run the tests when you make any change in your tests. With create-react-app
, you don't need to run the test
command again and again.
After making the above change, check the output in the terminal. Not only our test will pass but you will see the following HTML output:
Before we write more tests, lets first explore some features of the React Testing Library that can be used to write our tests.
Selecting Elements
In our first test, we used the getByText
method which is one of the query methods provided by React Testing Library. After we have selected an element, we can do different assertions or simulate user interactions to interact with those elements.
The getByText
method takes a string that is used to find the element. One caveat of passing a string as an argument to getByText
method is that string needs to be an exact match, meaning in our first test, if we had written
getByText('Hello');
instead of
getByText('Hello World');
our test would have failed because the getByText
method would have failed to find the h1
element. When the getByText
method fails to find an element, it throws an error.
You could also pass a regular expression to getByText
for a partial match. So in our first test, we could write
getByText(/Hello/);
to select the h1
element.
React Testing Library provides different query methods for selecting elements. Each of those query methods belong to one of the following categories:
getBy\*
getByAll\*
queryBy\*
queryAllBy\*
findBy\*
findAllBy\*
getByText
that we used in our first test belongs to the first category of queries, i.e. getBy*
.
There are other methods that can be used to select elements. Following methods belong to the getBy*
category of queries.
getByText
getByRole
getByLabelText
getByPlaceholderText
getByAltText
getByDisplayValue
There are similar methods available in other categories as well, for example, the following methods can be found under the queryBy*
category.
queryByText
queryByRole
queryByLabelText
queryByPlaceholderText
queryByAltText
queryByDisplayValue
Surely you see a pattern here. Other categories have similar methods.
To learn all about the individual methods, check the official docs and API reference, for example for getByText.
Selecting Elements using the id Attribute
If none of the above mentioned query methods allows you to select any particular element, you could add a data-testid
attribute on the element that you want to select and then select that element using getByTestId
function.
Example:
<span data-testid="mySpan">Hello</span>
span
element above can be selected as shown below:
const el = getByTestId('mySpan');
The Differences Between Different Query Methods
Let's take a look at the differences between the query methods belonging to getBy*
category. Generally, the same methods belonging to different categories also work the in same way - the only difference will be that of the category.
We will take a look at the differences between different categories later.
getByText
: Selects an element that contains the text passed as an argument to this methodgetByRole
: Selects an element by the accessibility rolegetByLabelText
: Selects an element associated with the label whosehtmlFor
attribute matches the string passed to this method as an argument.getByPlaceholderText
: Selects the element by its placeholder text.getByAltText
: Selects an element (normally an img element) with the matching value ofalt
attribute.getByDisplayValue
: Returns the input, textarea, or select element that has the matching display value.
The Differences Between Different Query Categories
Now lets take a look at the differences between different query categories:
getBy\*
: Query methods in this category return the first matching element or throw an error if no match was found or if more than one match was found.getByAll\*
: Query methods in this category return an array of all matching elements or throw an error if no elements matchedqueryBy\*
: Query methods in this category return the first matching element or returnnull
if no elements match. They also throw an error if more than one match is foundqueryAllBy\*
: Query methods in this category return an array of all matching elements or return an empty array if no elements matchfindBy\*
: Query methods in this category return a promise which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of1000ms
findAllBy\*
: Query methods in this category return a promise which resolves to an array of elements when any elements are found which match the given query. The promise is rejected if no elements are found after a default timeout of1000ms
When To Use Which Query Variant
Just in case its not clear from the above description, if you want to select an element that is rendered after an asynchronous operation, use the findBy*
or findByAll*
variants.
If you want to assert that some element should not be in the DOM, use queryBy*
or queryByAll*
variants. Otherwise use getBy*
and getByAll*
variants.
Assertive Functions
In our first test written above, we used a method named toBeInTheDocument
to check if the h1
element was in the DOM or not. This is an assertive function that is used on the right side of an assertion.
test('render h1 element', () => {render(<App />);screen.debug();expect(screen.getByText('Hello World')).toBeInTheDocument();});
Jest provides many assertive function but React Testing Library adds more assertive functions in an extra package named jest-dom.
This package comes pre-installed in a project created with create-react-app
.
Assertive functions provided by this package are:
toBeDisabled
toBeEnabled
toBeEmpty
toBeEmptyDOMElement
toBeInTheDocument
toBeInvalid
toBeRequired
toBeValid
toBeVisible
toContainElement
toContainHTML
toHaveAttribute
toHaveClass
toHaveFocus
toHaveFormValues
toHaveStyle
toHaveTextContent
toHaveValue
toHaveDisplayValue
toBeChecked
toBePartiallyChecked
toHaveDescription
After the above theory, lets write some more tests.
Testing Multiple Elements
We saw above that there are query categories that allow us to select multiple elements.
Lets write a test to check if there is a specific number of list items in an ul
element.
Before we write the test, we need to change out App
component
function App() {return (<div><ul className="animals"><li>Cat</li><li>Whale</li><li>Lion</li><li>elephant</li><li>Rhino</li></ul></div>);}
Now our component renders a list of animals and we will write a test to assert the following:
The
ul
element should be in the document.The
ul
element should have a class namedanimals
.There should be exactly 5 list items in the
ul
element.
import React from 'react';import { render, screen } from '@testing-library/react';import App from './App';test('list contains 5 animals', () => {render(<App />);const listElement = screen.getByRole('list');const listItems = screen.getAllByRole('listitem');expect(listElement).toBeInTheDocument();expect(listElement).toHaveClass('animals');expect(listItems.length).toEqual(5);});
In the above test we are using the getByRole
and getAllByRole
queries to select the ul
element and li
element respectively. Then we are making the three assertions that were mentioned before the test.
Notice the assertive functions used in the assertions in the above test: toBeInTheDocument
and toHaveClass
are from jest-dom
, whereas toEqual
is from Jest itself.
Asynchronous Tests
Now lets write a test that will test whether a certain component is rendered after an asynchronous request to jsonplaceholder
api to fetch a single user.
Before we write the test, lets change our App
component and also create a User
component that will be rendered once the user has been fetched from the API.
User Component
function User(props) {const { name, email } = props.user;return (<div className="person"><h3>{name}</h3><span>{email}</span></div>);}
App Component
function App() {const [user, setUser] = React.useState(null);const [error, setError] = React.useState('');React.useEffect(() => {fetch('https://jsonplaceholder.typicode.com/users/1').then((response) => response.json()).then((user) => setUser(user)).catch((error) => setError(error.message));}, []);if (error) {return <span>{error}</span>;}return <div>{user ? <User user={user} /> : <span>Loading...</span>}</div>;}
The App
component makes a request to the API to fetch a user. Once the user has been fetched, it is saved in the state. If there's an error, it is also saved in the state and the error message is rendered instead of the User
component.
To test whether the user is fetched from the API and rendered in the DOM, we will mock the fetch
function so that we don't make an actual request while testing.
To mock the fetch
function, we will provide our own implementation of the fetch
function.
window.fetch = jest.fn(() => {const user = { name: 'Jack', email: 'jack@email.com' };return Promise.resolve({json: () => Promise.resolve(user),});});
This replaces the original fetch
method with a custom method which will return a promise but which will not send an actual Http request.
For more details on how to mock function or modules in Jest, you can refer to their official documentation which covers mocking in detail.
When we render the App
component, our component will now use the mocked version of the fetch
function.
Now lets write some tests. Instead of writing one test, we will write three tests to assert the following:
While the request is in progress, a loading text should be visible.
Therafter, the user's name should be rendered in the document.
In case of an error, an error message should be rendered.
Lets write our tests one by one.
The first test will be related to the loading text.
import {render,screen,waitForElementToBeRemoved,} from '@testing-library/react';test('loading text is shown while API request is in progress', async () => {render(<App />);const loading = screen.getByText('Loading...');expect(loading).toBeInTheDocument();await waitForElementToBeRemoved(() => screen.getByText('Loading...'));});
A couple of things that should be noted in the above test:
We're using an
async
functionWe're using the
waitForElementToBeRemoved
function
When testing components that contain an asynchronous operation, like an API request in our App
component, you might see a warning message which says
Warning: An update to App inside a test was not wrapped in act(...)
This warning means that something happened to our component when we weren't expecting anything to happen. For details on this warning, the following blog post by the creator of React Testing Library might be interesting: Fix the "not wrapped in act(...)" warning.
In our case, we get this warning because after we assert that the loading text should be in the document, our component's state is updated and the component is re-rendered. Our promise (in the fake fetch
method) does resolve instantly after all.
We can avoid this warning by waiting for the component to re-render, after the API request, before ending our test. To wait for a component to re-render, we have used the waitForElementToBeRemoved
function. This function will not let our test finish until the loading text has disappeared from the DOM
, which only happens in case of an error or a successful API request.
The waitForElementToBeRemoved
function returns a Promise
so we need to await
it. And in order to use the await
keyword, we have used an async
function.
Now lets write the second test to assert that user is successfully fetched and the user's name is rendered in the DOM
.
test("user's name is rendered", async () => {render(<App />);const userName = await screen.findByText('Jack');expect(userName).toBeInTheDocument();});
Since our mocked version of fetch
function returns a Promise
that resolves with the following user object
const user = { name: 'Jack', email: 'jack@email.com' };
in our test, we assert that an element with the text "Jack" should be rendered in the DOM
.
Also notice the use of findByText
: We are using this query method because query methods in the findBy*
category should be used to select elements that are rendered in the DOM
after an asynchronous operation.
We can't use queryBy*
or getBy*
variants here. We also need to await
the result of screen.findByText("Jack")
, so we used an async
function.
Now lets write our final asynchronous test which will assert that in case of an error, our App
component shows an error message.
test('error message is shown', async () => {window.fetch.mockImplementationOnce(() => {return Promise.reject({ message: 'API is down' });});render(<App />);const errorMessage = await screen.findByText('API is down');expect(errorMessage).toBeInTheDocument();});
The only thing to be noticed here is that we are using a different version of our mocked fetch
function. This is needed because our initial mocked version of the fetch
function doesn't reject the Promise
. So to force our API request to fail, we reject the Promise
with an object containing a message
property.
The value of this message
property is saved in the state of our App
component and displayed to the user when our API request fails. Hence we're checking for this message with findByText("API is down")
.
Grouping Tests in a Test Suite
We wrote three tests to test our App
component that contains asynchronous code. When we run our tests, there is no indication on the terminal that these three tests are related to each other.
We can group them together in a test suite by wrapping them with the describe
function provided by Jest.
The following is the final code of our three tests grouped together in a test suite.
import React from 'react';import {render,screen,waitForElementToBeRemoved,} from '@testing-library/react';import App from './App';window.fetch = jest.fn(() => {const user = { name: 'Jack', email: 'jack@email.com' };return Promise.resolve({json: () => Promise.resolve(user),});});describe('Testing App Component', () => {test('loading text is shown while API request is in progress', async () => {render(<App />);const loading = screen.getByText('Loading...');expect(loading).toBeInTheDocument();await waitForElementToBeRemoved(() => screen.getByText('Loading...'));});test("user's name is rendered", async () => {render(<App />);const userName = await screen.findByText('Jack');expect(userName).toBeInTheDocument();});test('error message is shown', async () => {window.fetch.mockImplementationOnce(() => {return Promise.reject({ message: 'API is down' });});render(<App />);const errorMessage = await screen.findByText('API is down');expect(errorMessage).toBeInTheDocument();});});
Now if you run the tests or if jest is running in watch mode, you will see the following output in the terminal.
Notice how our tests are grouped together under Testing App Component
.
Simulating User Interactions
React Test Library provides an fireEvent
API which can be used to trigger events like change
on an input
element.
React Testing Library also provides another API for simulating use interactions in a separate package named user-event. This API builds on top of the fireEvent
API and contains functions which mimic browser behavior more closely than the fireEvent
API. For example, fireEvent.change()
triggers only a change
event whereas UserEvent.type
triggers change
, keyDown
, keyPress
and keyUp
events.
In the following tests, we will use the user-event
library instead of the fireEvent
API for simulating user interactions with HTML elements.
For this, let's change our App
component once again to render a counter that can be incremented using the "increment" and "decrement" buttons.
function App() {const [counter, setCounter] = React.useState(0);const increment = () => {setCounter((prevCounter) => ++prevCounter);};const decrement = () => {setCounter((prevCounter) => --prevCounter);};return (<div><h2 data-testid="counter">{counter}</h2><button onClick={decrement}>Decrement</button><button onClick={increment}>Increment</button></div>);}
We will write two tests to assert that counter
is incremented and decremented correctly. We will also group both our tests in a single test suite.
Note the data-testid
attribute on the h2
element. As mentioned before, this attribute can be used to select elements using the getByTestId
query method. In the following two tests, we will use this method to select the h2
element.
import React from 'react';import { render, screen } from '@testing-library/react';import UserEvent from '@testing-library/user-event';import App from './App';describe('Testing App Component', () => {test('counter is incremented on increment button click', () => {render(<App />);const counter = screen.getByTestId('counter');const incrementBtn = screen.getByText('Increment');UserEvent.click(incrementBtn);UserEvent.click(incrementBtn);expect(counter.textContent).toEqual('2');});test('counter is decremented on decrement button click', () => {render(<App />);const counter = screen.getByTestId('counter');const decrementBtn = screen.getByText('Decrement');UserEvent.click(decrementBtn);UserEvent.click(decrementBtn);expect(counter.textContent).toEqual('-2');});});
The first test checks if the counter
is incremented correctly by simulating a click event on the increment button twice. As the initial value of counter
is zero, after clicking the increment button twice, our counter's value should be 2.
That is what we are checking in our assertion.
Note that we have passed a string "2"
to the toEqual
function instead of a number 2
. The reason is that we are using the textContent
property of an HTML element to get the value of the counter.
Since the textContent
property always yields a string, we could either convert the return value of counter.textContent
to a number and then assert that it should be equal to 2
or we could just use a string "2"
.
In the second test, we are testing the decrement
functionality by clicking the decrement button twice and asserting that counter's value should be "-2".
You might have expected that, as we decrement the counter after incrementing it twice in the first test, the counter's value should be "0" instead of "-2" but that's not the case!
That's because the React Testing Library automatically unmounts the React component tree after each test.
It should be noted that this automatic cleanup is done only when we use the React Testing Library with a testing framework that has an afterEach
global function. As Jest has such a function, our component tree is automatically cleaned up after each test. That is why counter's value is "-2" after the second test.
Testing Callbacks
Consider a scenario where you have a component that represents an input
element. This component receives two props:
The value of the
input
elementAnd a callback function which will be used to handle the
onChange
event on theinput
element
In addition, this input
element is a controlled, meaning its value is derived from the state of the component in which it is rendered.
Now you want to test whether the value of the input element is updated correctly and also make sure that the callback function is called each time the value of the input
changes.
We will write a couple of tests for this case but before we do that, lets create an Input
component and also render this Input
component in the App
component, passing in the required props from the App
component to the Input
component.
Input component
export function Input(props) {const { handleChange, inputValue } = props;return <input onChange={handleChange} value={inputValue} />;}
App component
function App() {const [inputValue, setInputValue] = React.useState('');const handleChange = (event) => {setInputValue(event.target.value);};return (<div><Input handleChange={handleChange} inputValue={inputValue} /></div>);}
Now lets write our tests.
In the first test, we will assert that the input value is updated correctly.
import UserEvent from '@testing-library/user-event';test('input value is updated correctly', () => {render(<App />);const input = screen.getByRole('textbox');UserEvent.type(input, 'React');expect(input.value).toBe('React');});
We are selecting the input
element by its role. What's great about the getByRole
query method is that if you pass any role to this method that is not associated with any element in your component, it will suggest you the available roles once you run the test.
After selecting the input
element, we are using the user-event
API to trigger an onChange
event on the input
element. Since we wrote "React"
in the input component, we expect its value to be "React"
.
For our second test, we will test whether or not the handleChange
callback function is called every time input value is changed.
To test this, we will mock the handleChange
function which is passed to the Input
component so that we can track how many times it was called. And instead of rendering the App
component, we will render the Input
component, passing in the mocked version of the handleChange
function.
import UserEvent from '@testing-library/user-event';import { Input } from './App';test('call the callback every time input value is changed', () => {const handleChange = jest.fn();render(<Input handleChange={handleChange} inputValue="" />);const input = screen.getByRole('textbox');UserEvent.type(input, 'React');expect(handleChange).toHaveBeenCalledTimes(5);});
As we will render the Input
component instead of App
, we need to import the Input
component in our test file. At the start of our test, we have mocked the handleChange
function and then passed it as a prop to the Input
component.
We again are using the user-event
API to trigger an onChange
event on the input
element and after that we are asserting that the handleChange
function has been called 5 times because we typed "React"
(= 5 characters) in the input
element.
Final Thoughts
This is it for this tutorial. I hope this tutorial was good enough to get you started with testing React apps.
Of course, there are lots of test scenarios that aren't covered in this tutorial and can't be covered in one tutorial.
One such scenario is testing components that are connected to a Redux store. Everything you learned in this tutorial still applies though - you just need to mock the Redux store which can be done using the Redux Mock Store library. Once you have mocked the Redux store and the component you want to test receives the state as props from the mocked Redux store, you can test your components in the same way as you learned in this tutorial.
And of course there are other scenarios as well - with the foundation provided in this article, you should be well prepared to dig deeper into testing though.