How to Confidently Write Unit Tests using React Testing Library

Yogesh Chavan - Dec 2 '22 - - Dev Community

In this tutorial, we will learn how to confidently write unit tests using Testing Library which is a very popular React testing library for writing unit tests.

So let's get started.

If you want to learn React Testing Library from scratch and want to learn how to confidently test an entire application with functional components including hooks, mock API calls, and set up test coverage, then do enroll in my MERN stack course.

Why Do You Need to Write Unit Tests?

You might think that there is no need of writing so many unit test cases and wasting time, instead, we can manually test the application.

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 and even a smaller change can break the application If all the functionality is not tested properly.

That's the reason, it's recommended to write unit test cases covering all those scenarios which you're manually doing as a user.

So by executing just a single command you will be able to know If something is broken in your application If some test is failing.

What is the React Testing Library?

The React Testing Library has a set of packages that helps you test UI components in a user-centric way which means just like how the user interacts with the various elements displayed on the page.

It means what happens 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 he/she might miss testing some scenarios when the application size 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 your web pages are interacted by the users.

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, where you might be checking the value of state once you click any button or you might be checking the prop value If something changes.

These types of checks are not necessary for testing with react testing library. Instead, in react testing library you check the behavior of DOM when the user clicks on a button or submits a form and so on.

How to Setup a React Project with Vite

We will be using Vite which is the most popular and a faster alternative to create-react-app.

The reason for using vite, is that create-react-app becomes slow when the application grows and takes a lot of time to refresh the page when we make any change in the application code, and by default, it also adds a lot of extra unnecessary 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.

Vite requires Node.js version 14.18+, so make sure to install 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 will 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 enter testing-library-demo or any name of your choice.
  • For framework, select React from the list of options
  • For variant, select JavaScript from the list of options

1_setup.gif

Once the project is created, you can open that project in your favorite IDE.

The project folder structure will look like this:

2_folder_structure.png

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.

3_app_started.png

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.

4_app_loaded.png

So let's see how we can set up Testing Library in our Vite project.

How to Set Up the Testing Library and Jest in a React Project

We cannot only use the testing library alone, but we also need to install Jest which exposes extensively used global expect function and other things which help to make assertions in our test cases.

To set up Testing library and Jest, we need to install 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
Enter fullscreen mode Exit fullscreen mode

OR

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
Enter fullscreen mode Exit fullscreen mode

We're 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 interaction through 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 etc which makes it easy to test for DOM elements which you will see later in this tutorial.

We have also added 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, 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"
Enter fullscreen mode Exit fullscreen mode

Your package.json file will look like this now:

5_package_json.png

If you're not using vite for creating the React app, then you will be using the following test script:

"test": "jest --watch"
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

Now, open the vite.config.js file and add a new test object as shown in the below screenshot:

9_vite_config.png

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 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
Enter fullscreen mode Exit fullscreen mode

OR

npm install bootstrap@5.2.3 react-bootstrap@2.6.0 react-select@5.6.1
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

Your src/main.jsx file will look like this now:

6_bootstrap_css.png

We don't need index.css and App.css files so you can delete those files.

Now, create a components folder inside the src folder and create a register folder inside the components folder, and 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 link in the Register.jsx file and inside register.css file add contents from this link.

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;
Enter fullscreen mode Exit fullscreen mode

Now, If you run the application by executing the yarn run dev or npm run dev command, you will see the following screen:

7_register_page.png

This tutorial is for teaching 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 my this article to understand it better.

Also, instead of managing the state and onChange handler yourself, you can use a very popular react-hook-form library.

It also allows you to add validations to your code without writing much code. Check out my this article, If you want to learn 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 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:

8_queries.png

To summarise:

  • To select a single DOM element, you can use getBy, findBy, or queryBy query
  • To select multiple DOM elements, you can use getAllBy, findAllBy or queryAllBy query
  • getBy and findBy returns an error If there is no match or more than one match
  • queryBy returns null If there is no match and returns an error If there is more than one match
  • findBy works well with asynchronous code but not with getBy and queryBy
  • getAllBy returns an error, If there is no match and returns an array of matches for one or more than one match
  • findAllBy returns an error, If there is no match and returns an array of matches for one or more than one match
  • queryAllBy 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, we'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();
  });
});
Enter fullscreen mode Exit fullscreen mode

Note that, we're using vite so the filename has to end with .jsxextension even for test files.
If you're not using vite then you can end the filename with .js extension.

Now, If you execute the npm run test or yarn run test command you will see that the test passes.

1_test_result.png

In the above code, we're first rendering the Register component using 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:

10_role.gif

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();
Enter fullscreen mode Exit fullscreen mode

You can see a list of all available getBy,findBy or queryBy methods by adding a dot after screen like this:

11_query_methods.gif

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're getting an error message as shown below.

12_error_message.gif

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 already installed previously.

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);
});
Enter fullscreen mode Exit fullscreen mode

Your register.test.jsx file will look like this:

13_new_test.png

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 and is 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();
});
Enter fullscreen mode Exit fullscreen mode

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:

14_debug_output.png

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 and before that only we're 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:

15_await_event.png

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.

error_message_dom.png

so we can use it in our assertion like this:

const alertElement = screen.getByRole("alert");
expect(alertElement).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

And now, you can see that the second test is also successful.

16_success_test.png

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 using userEvent as you may not know when the action will be completed

Let's Write Some More Tests

Now, we're done with adding 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();
});
Enter fullscreen mode Exit fullscreen mode

Here, instead of using

expect(alertElement).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

we're using

expect(alertElement).not.toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

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.

17_failed_test.png

So, the test is not failing because of our assertion but 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 screenshot shown before:

8_queries.png

The getBy method throws an error If it does not find the matching element so instead of using getBy we need to use queryBy which 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();
});
Enter fullscreen mode Exit fullscreen mode

Now, If check the console, you will see that the test passes successfully.

18_test_passed_query.png

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();
});
Enter fullscreen mode Exit fullscreen mode

Now, If you see the console, you will see that the test passes successfully.

Now, 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>
Enter fullscreen mode Exit fullscreen mode

so your code will look like this:

20_subtitle.png

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

21_failed_multiple.png

The test failed because in the first test we're finding the Register heading text as shown below:

screen.getByRole("heading")
Enter fullscreen mode Exit fullscreen mode

And as you know, getBy returns an error when you have more than one match.

Here, we've 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();
});
Enter fullscreen mode Exit fullscreen mode

Assuming you added an extra data-testid attribute to your JSX like this:

<h2 className="title" data-testid="title">
     Register
</h2>
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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.

22_fixed_failing_test.png

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 });
Enter fullscreen mode Exit fullscreen mode

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:

23_accurate_subheading.gif

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
})
Enter fullscreen mode Exit fullscreen mode

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();
 });
Enter fullscreen mode Exit fullscreen mode

24_all_passed.png

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:

25_extension_demo.gif

So 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 we'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
})
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

React Testing library is amazing and very popular to test React applications.

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 this 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.

You will also get my popular Mastering Modern JavaScript ebook and Mastering Redux course for FREE during this sale.

Today is the last day of the extended sale with a 70% discount. So don't miss this last opportunity and enroll in the course now.

Want to stay up to date with regular content regarding JavaScript, React, Node.js? Follow me on LinkedIn.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .