In this tutorial, you will learn how to confidently write unit tests using the Testing Library. It is a very popular React testing library for writing unit tests.
So let's get started.
What We'll Cover:
- Why Do You Need to Write Unit Tests?
- What is the React Testing Library?
- What Not to Test with the Testing Library
- How to Setup a React Project with Vite
- How to Set Up the Testing Library and Jest in a React Project
- How to Create the UI for Testing
- How to Write Unit Test Cases
- Let's Write Some More Tests
- Conclusion
If you want to learn the React Testing Library in depth from scratch, you can check out my MERN stack course.
Why Do You Need to Write Unit Tests?
You might think that you don't need to write so many unit test cases and that it's a waste of time. Perhaps you can manually test the application instead.
Well, you're right – you can certainly do that. But as the application grows, it might be difficult to test all the scenarios in the application and you might miss something. Even a small change might break the application if all the major functionality is not tested properly.
That's why I recommend writing unit test cases covering all those scenarios which you're manually going through as a user.
So by executing just a single command you will be able to know if something is broken in your application or if some test is failing.
What is the React Testing Library?
The React Testing Library has a set of packages that help you test UI components in a user-centric way. This means it tests based on how the user interacts with the various elements displayed on the page.
So what happens is when the user clicks any button or types in any of the input textboxes, that interaction is tested using this testing library.
So instead of the user doing this testing manually (which takes a lot of time, and the user might miss testing certain scenarios when the application grows), the testing is done by writing unit test cases and executing them by just a single command.
What Not to Test with the Testing Library
Testing Library encourages you to avoid testing implementation details like the internals of a component you're testing.
The guiding principles of this library emphasize a focus on tests that closely resemble how users interact with your web pages.
You may want to avoid testing the following implementation details:
- Internal state of a component
- Internal methods of a component
- Lifecycle methods of a component
- Child components
So if you have experience with enzyme testing, you might be checking the value of state once you click any button or you might be checking the prop value If something changes.
But these types of checks are not necessary for testing with the React testing library. Instead, in the React testing library, you check the behavior of the DOM when the user clicks on a button or submits a form and so on.
How to Setup a React Project with Vite
To set up our app that we'll test, we'll be using Vite. It's a popular and faster alternative to create-react-app.
We'll use Vite because create-react-app
becomes slow when the application grows and takes a lot of time to refresh the page when we make any changes in the application code. Also, by default, it also adds a lot of extra packages which we rarely need.
Vite just rebuilds the things we changed, instead of rebuilding the entire application which saves a lot of time during development.
Keep in mind that Vite requires Node.js version 14.18+, so make sure to install a Node version greater than or equal to 14.18.
The easiest and simplest way to install and switch Node.js versions is to use nvm.
Even if you're using create-react-app
, all the tests you will learn in this tutorial should run exactly the same without any errors.
To create a new Vite project with React, execute the npm init vite
command from the terminal.
It will ask you the project name
, framework
, and variant
.
- For
project name
, you can entertesting-library-demo
or any name of your choice. - For
framework
, selectReact
from the list of options - For
variant
, selectJavaScript
from the list of options

Once the project is created, you can open that project in your favorite IDE.
The project folder structure will look like this:

Now, execute the yarn
or npm install
command to install all the packages from the package.json
file.
Once all the packages are installed, you can execute the yarn run dev
or npm run dev
command to start the created React application.

Now, if you access the displayed URL http://127.0.0.1:5173/
you will be able to see the default React application created using Vite.

So let's see how we can set up the Testing Library in our Vite project.
How to Set Up the Testing Library and Jest in a React Project
You can't just use the testing library alone – you also need to install Jest. It exposes an extensively used global expect function and other things which help you make assertions in your test cases.
To set up Testing library and Jest, you need to install the Jest and Testing Library packages as dev dependencies.
So execute the following command from the testing-library-demo
folder:
yarn add @testing-library/jest-dom@5.16.5 @testing-library/react@13.4.0 @testing-library/user-event@14.4.3 jest@29.3.1 jest-environment-jsdom@29.3.1 vitest@0.25.3 --dev
or with npm:
npm install @testing-library/jest-dom@5.16.5 @testing-library/react@13.4.0 @testing-library/user-event@14.4.3 jest@29.3.1 jest-environment-jsdom@29.3.1 vitest@0.25.3 --save-dev
I'm mentioning versions here for each package which are the latest versions at the time of writing this tutorial. So even if there is a newer breaking version release that happens for any of the packages in the future, your code will not break.
Here, we're installing the jest-environment-jsdom
library because we will be running tests in the node environment. But we're testing browser interactions through the DOM – so to inform Jest about that, we need to add this library.
The @testing-library/jest-dom
library is required because it contains assertions like toBeInTheDocument
, toHaveBeenCalled
, and others which make it easy to test for DOM elements, which you will see later in this tutorial.
We have also added the vitest
package which is only required when you're using Vite for the application.
You don't need it if you're using create-react-app
or your own webpack configuration.
Now that we have installed the required packages, let's add a script in the package.json
file to run the tests.
Open the package.json
file and add the test
script inside it like this:
"test": "vitest"
Your package.json
file will look like this now:

If you're not using vite
for creating the React app, then you will be using the following test script:
"test": "jest --watch"
Now, create a new file in the root of your project (testing-library-demo
) with the name setupTests.js
and add the following code inside it:
import "@testing-library/jest-dom";
Now, open the vite.config.js
file and add a new test
object as shown in the below screenshot:

How to Create the UI for Testing
Before writing tests, we need to have some components to test.
So let's create a simple registration page with some checkboxes, input fields, select dropdown, and buttons so we can write test cases for it.
We will use react-bootstrap to create the UI elements so we don't have to write all the CSS from scratch.
Install bootstrap
and react-bootstrap
by executing the following command from the terminal:
yarn add bootstrap@5.2.3 react-bootstrap@2.6.0 react-select@5.6.1
or with npm:
npm install bootstrap@5.2.3 react-bootstrap@2.6.0 react-select@5.6.1
Bootstrap provides a base CSS which we need for the UI to look nice, so we're also installing Bootstrap along with react-bootstrap.
Once installed, open src/main.jsx
and add an import for the Bootstrap CSS file before any of your other CSS files as shown below:
import "bootstrap/dist/css/bootstrap.min.css";
Your src/main.jsx
file will look like this now:

We don't need the index.css
and App.css
files so you can delete them.
Now, create a components
folder inside the src
folder and create a register
folder inside the components
folder. Inside the register
folder, create Register.jsx
and register.css
files.
So your Register.js
file path will be src/components/register/Register.js
.
Add the content from this repo in the Register.jsx
file and inside register.css
file add contents from this repo.
Now, open the App.jsx
file and add the following contents inside it:
import Register from "./components/Register";
function App() {
return <Register />;
}
export default App;
Now, if you run the application by executing the yarn run dev
or npm run dev
command, you will see the following screen:

This tutorial is focused on the testing library, so I'm not going to explain the code from the Register.js
file as it's basic React code. But If you're not familiar with React hooks, you can check out this article to understand it better.
Also, instead of managing the state and onChange handler yourself, you can use the very popular react-hook-form library.
It also allows you to add validations to your code without writing much code. Check out this article if you want to learn about it in detail.
Now, we're all set to write unit test cases, so let's get started.
How to Write Unit Test Cases
Before writing test cases, you should be aware of the different queries which you can make to access elements on the page.
The Testing Library provides a set of queries which you can see in the below screenshot:

To summarise:
- To select a single DOM element, you can use the
getBy
,findBy
, orqueryBy
query - To select multiple DOM elements, you can use the
getAllBy
,findAllBy
orqueryAllBy
query getBy
andfindBy
return an error if there is no match or more than one matchqueryBy
returns null if there is no match and returns an error if there is more than one matchfindBy
works well with asynchronous code but not withgetBy
andqueryBy
getAllBy
returns an error if there is no match and returns an array of matches for one or more than one matchfindAllBy
returns an error if there is no match and returns an array of matches for one or more than one matchqueryAllBy
returns an empty array for no match and returns an array of matches for one or more than one match
So if you don't want your test to fail if the element is not displayed on the UI, then always use queryBy
or queryAllBy
.
In other words, only use the queryBy
or queryAllBy
queries for asserting that an element cannot be found or is hidden.
Now that you're familiar with query methods, let's start writing test cases for the Register
component.
Create a new register.test.jsx
file in the src/components/register
folder with the following content inside it:
import { render, screen } from "@testing-library/react";
import Register from "./Register";
describe("Register component", () => {
it("should render Register component correctly", () => {
render(<Register />);
const element = screen.getByRole("heading");
expect(element).toBeInTheDocument();
});
});
Note that we're using Vite so the filename has to end with the .jsx
extension even for test files. If you're not using vite then you can end the filename with the .js
extension.
Now, if you execute the npm run test
or yarn run test
command you will see that the test passes.

In the above code, we're first rendering the Register
component using the render
method provided by the testing library.
As we have an h1 element with Register
text in the Register
component, we're using the screen.getByRole
method to get the DOM element of the role heading
.
If you don't know what role to use in the getByRole
method, then you can use some random name and the testing library will show you all the available roles for each DOM element for that component as shown below:

Once we get that element using the getByRole
method, then we're making an assertion to check if that element exists in the DOM using:
expect(element).toBeInTheDocument();
You can see a list of all available getBy
,findBy
or queryBy
methods by adding a dot after screen
like this:

Now, we have added one test to check if the Register
component is getting rendered correctly or not.
Let's write some more tests.
If you run the application by running the yarn dev
command, you will see that, once you click on the Register
button without filling out all the details, you get an error message as shown below.

So now, we need to test the same by writing a test case.
For that, we can use userEvent
from the @testing-library/user-event
package which we've already installed.
Now, add a new test in your register.test.jsx
file as shown below:
it("should show error message when all the fields are not entered", () => {
render(<Register />);
const buttonElement = screen.getByRole("button");
userEvent.click(buttonElement);
});
Your register.test.jsx
file will look like this:

So we're triggering the click event for the Register
button in the above code.
Now, we need to find the element with the error message so we can add assertions for it in the test.
The error message is actually an Alert
component from react-bootstrap
which is not visible initially. It's only displayed when we submit the form without filling in all the data.
In such a case, we can call the screen.debug
method to see the structure of the DOM at that moment when we trigger the click event.
So change the test case as shown below:
it("should show error message when all the fields are not entered", async () => {
render(<Register />);
const buttonElement = screen.getByRole("button");
userEvent.click(buttonElement);
screen.debug();
});
Note that we have added screen.debug
at the end of the test.
Now, if you run the yarn run test
or npm run test
, you will see the following DOM structure:

As you can see from the screenshot, you directly see the name input label inside the form tag after the Register
heading.
So we're not able to see the error message even if we have triggered the click event for the button.
This is because it takes some time to execute the validation code from the handleFormSubmit
method. Before that we're only using the screen.debug
method so we don't see the error message.
So to fix this, we can wait using async/await.
So declare the test function as async
and before the userEvent.click(buttonElement)
add an await
keyword like this:

Now, if you check the console, you will be able to see the text All the fields are required.
inside a div with the role alert
.

So we can use it in our assertion like this:
const alertElement = screen.getByRole("alert");
expect(alertElement).toBeInTheDocument();
And now, you can see that the second test is also successful.

Some points to note:
- Always remember to remove the
screen.debug
statement once you're done with your assertions, and never keep it in your code. - Always add an
await
keyword before triggering any event usinguserEvent
as you may not know when the action will be completed.
Let's Write Some More Tests
Now that we're done with adding these two tests, we can add one more test to check if there is no error when the page is loaded like this:
it("should not show any error message when the component is loaded", () => {
render(<Register />);
const alertElement = screen.getByRole("alert");
expect(alertElement).not.toBeInTheDocument();
});
Here, instead of using this:
expect(alertElement).toBeInTheDocument();
we're using this:
expect(alertElement).not.toBeInTheDocument();
Because we want the alert element to be not present on the component load.
But if you check the console, you will see that the test is failing.

So, the test is not failing because of our assertion. Rather, it's failing because it can't find an element with role alert
on page load which is expected, as there will not be any error on page load.
But how we can make the test pass?
If you remember from the list of queries in the screenshot shown before:

The getBy
method throws an error if it does not find the matching element. So instead of using getBy
we need to use queryBy
. It does the same thing but it does not throw an error when there is no matching element.
So let's modify our test case to the below code:
it("should not show any error message when the component is loaded", async () => {
render(<Register />);
const alertElement = screen.queryByRole("alert");
expect(alertElement).not.toBeInTheDocument();
});
Now, if you check the console, you will see that the test passes successfully.

Now, let's write a test for successful registration when we fill out all the required fields.
it("should show success message when the registration is successful.", async () => {
render(<Register />);
const buttonElement = screen.getByRole("button");
await userEvent.click(buttonElement);
const alertElement = screen.getByRole("alert");
expect(alertElement).toBeInTheDocument();
});
Now if you see the console, you will see that the test passes successfully.
Let's add a subheading to the registration page and see what happens when we run the tests again.
Add the following heading inside the Form
tag in the Register.jsx
file:
<h6 className="subtitle">
Please enter your details below to register yourself.
</h6>
So your code will now look like this:

Now, if you run the tests again, you will see that one test fails:

The test failed because in the first test we're finding the Register
heading text as shown below:
screen.getByRole("heading")
And as you know, getBy
returns an error when you have more than one match.
Here, we have two headings on the Register
component so the test failed.
So how can we fix it?
To fix it we need to identify how to accurately select elements while writing tests.
I have seen many developers changing the HTML structure by adding some testid
so they can get the test passed like this:
it("should render Register component correctly", () => {
render(<Register />);
const element = screen.getByTestId("title");
expect(element).toBeInTheDocument();
});
Assuming you added an extra data-testid
attribute to your JSX like this:
<h2 className="title" data-testid="title">
Register
</h2>
This will work and make your all tests pass. But this is not the correct way.
Just to make your test pass, you should not change your JSX by adding some extra testid
or class
.
Instead, you should always try to use methods provided by screen
to make an accurate selection of DOM elements.
So now the question is how to make an accurate selection.
The getByRole
method accepts optional options which you can use like this:
const element = screen.getByRole("heading", { level: 2 });
As our main Register
heading is a h2
heading, we specifically said to select level 2
heading.
Now, if you update the first test case, you will see that all the tests are passing.

Now, let's add another test for testing the subheading.
As the subheading is of level 6, you can query it like this:
const element = screen.getByRole("heading", { level: 6 });
This will work, but there is another way we can target that element.
For that, you can install the testing playground Chrome browser extension.
Once it's installed, follow the below steps:
- open your Chrome dev tools using Ctrl + Alt + I or Cmd + Option + I (Mac)
- select the
Testing Playground
tab - Click the cursor pointer and select the subheading of the
Register
component as shown below:

As you can see, you will get the accurate DOM element query which you can use in your test like this:
screen.getByRole('heading', {
name: /please enter your details below to register yourself\./i
})
So you can write your test like this:
it("should test for presence of subheading in the component", () => {
render(<Register />);
const element = screen.getByRole("heading", {
name: /please enter your details below to register yourself\./i
});
expect(element).toBeInTheDocument();
});

Writing a test case for subheading is not necessary, because it does not affect the component behavior even if you don't test that. But just to show you how your tests will break for multiple elements, I have added that element on the UI along with its test case.
The Testing Playground
Chrome extension is really useful to find out the exact matching query for any of the UI elements.
So instead of using the screen.debug
method to see the DOM structure, you can use this Chrome extension to find out the role and other information for all the displayed elements as can be seen below:

As you can see, you can get any element by role, by placeholder text, or by label text with the methods provided by screen
.
Now that you're aware of the more specific query selectors, let's update other test cases to use the specific selectors.
Wherever we're using just screen.getByRole("button")
, replace it with the following:
screen.getByRole("button", {
name: /register/i
})
So now, if later someone adds another button in the same component, your test will not fail.
Your final register.test.jsx
file will look like this:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Register from "./Register";
describe("Register component", () => {
it("should render Register component correctly", () => {
render(<Register />);
const element = screen.getByRole("heading", {
level: 2
});
expect(element).toBeInTheDocument();
});
it("should test for presence of subheading in the component", () => {
render(<Register />);
const element = screen.getByRole("heading", {
name: /please enter your details below to register yourself\./i
});
expect(element).toBeInTheDocument();
});
it("should show error message when all the fields are not entered", async () => {
render(<Register />);
const buttonElement = screen.getByRole("button", {
name: /register/i
});
await userEvent.click(buttonElement);
const alertElement = screen.getByRole("alert");
expect(alertElement).toBeInTheDocument();
});
it("should not show any error message when the component is loaded", () => {
render(<Register />);
const alertElement = screen.queryByRole("alert");
expect(alertElement).not.toBeInTheDocument();
});
it("should show success message when the registration is successful.", async () => {
render(<Register />);
const buttonElement = screen.getByRole("button", {
name: /register/i
});
await userEvent.click(buttonElement);
const alertElement = screen.getByRole("alert");
expect(alertElement).toBeInTheDocument();
});
});
Conclusion
React Testing library is amazing and has become a very popular tool with which to test React applications.
Just remember that unlike enzyme testing library, you should not test for state changes when using React Testing Library.
So we have not written test cases to check if the state correctly changes after the user types some text in the name
, email
, or password
fields.
In React Testing Library you check the behavior of DOM when the user clicks on a button or submits a form and so on instead of testing the internal state of the component.
Thanks for Reading!
You can find the complete source code for this tutorial in this repository.
If you want to become the best MERN stack developer (full stack developer), then do check out my course.
- This is a pre-recorded video course that will be constantly updated for any future changes.
- In this course, you will learn how to create React and Node.js applications from scratch and build an amazing online learning platform.
- After learning through this course, you will be able to build any MERN stack application confidently and easily.
- There is a separate section in this course, where you will learn how to test your entire React application using React testing library and jest.
So do check out this amazing course.
Want to stay up to date with regular content regarding JavaScript, React, Node.js? Follow me on LinkedIn.