How to Write Unit Tests for React Apps?

Anurag Gharat
18 min read | Published on : Apr 03, 2023
Last Updated on : May 23, 2024





Table of Contents

React is a front-end JavaScript library built by Facebook for creating user interfaces for websites. According to the Stack Overflow 2022 survey, React was the second most common web technology used by Developers after Node.js.

React is based on component design, which means everything in React is a component. Hence every component should have its own test and should be tested in complete isolation. In this blog, we will look at React Testing Library which is one of the top testing libraries in React. React Testing Library is a JavaScript Testing Library for performing unit tests on React components. 

Before we check how to write Unit tests in React, let’s understand a little bit about Testing and what Unit Testing exactly means. 

Testing

In software development, testing is the process of confirming and validating the program's behaviour prior to its public release. Any anomalous conduct or mistake discovered throughout this procedure has been addressed and rectified. Testing can be done manually or automatically by a developer who writes tests that launch the application and look for issues while it's running.

Unit testing, end-to-end testing, integration testing, acceptance testing, and other testing methods are common. But unit testing is the most crucial one for our blog.

What is Unit Testing?

Every component of an application is tested separately during a process known as unit testing. For that component, all necessary dependencies are mocked.Determining if a component acts as expected and satisfies requirements is the primary goal of writing a unit test. Any mistake found now is critical since it reduces the amount of time needed for later debugging.

Everything in React is composed of components, as we have already covered. Every component has a unique state and carries out a particular task on the user interface. Errors or failures in one component will have an impact on its offspring components. It is therefore required to examine the behaviour of each component separately. It is exactly what React Testing Library does!

Resolve React errors faster with Zipy. Use advanced devtools and AI assistance.

Try for free

What is React Testing Library?

The React Testing Library is a lightweight testing package built on top of the DOM Testing Library. This library contains all the utilities and helpers to test React Components in a User-Centric way. The React testing library is a replacement for AirBnbs Enzyme. 

The Enzyme Library was used to test the component's state, props, and internal implementation details while React Testing Library only tests the DOM nodes and UI Elements. If you are using Enzyme in your application, it is possible to Migrate from Enzyme to React Testing Library

In React Testing Library we are not concerned with how the component behaves internally. We are only looking at how the component interacts with the user. So the library will not care if the component takes any route for finding the solution if the end result matches what the developer wants. Hence even in the future if a developer wishes to refactor his code, the test won’t fail if the output is still the same!

React Testing Library is used to write tests but we need a test runner to run our tests and give us a report on whether or not the tests failed. This is where we will use a JavaScript Testing Framework called Jest.

Jest is a JavaScript test runner that finds tests, executes them, and determines if they passed or failed. 

React Testing Library and Jest are not alternatives to each other. Both perform different tasks and work together to perform unit tests on React Components.

Since there are a lot of options for testing libraries to choose from, it is necessary to compare react testing libraries and find the one that fits the project requirement. 

Writing Unit Tests in React

Let's write some tests in the following part now that we have a better understanding of React's unit tests.

We assume you are familiar with React before starting this example. Before moving on to testing, we strongly advise that you familiarise yourself with React if you're new to it.

All the code that we will see is already present on my GitHub. You can access it here.

Setting up the Environment

Let’s set up a new React application using ‘create-react-app’

Using npx:

npx create-react-app react-testing

Using yarn:

yarn create react-app react-testing

We are using "create-react-app" as it has Jest and the React Testing Library pre-configured. A example test case has been written specifically for us.

Other approaches to building a React application would need installing the Testing packages independently. If you're building your own application, make sure to install Jest and the React Testing Library.

Running the Test

In our React application we can see that an ‘App.test.js’ file is already present in the src folder. This is a sample test file provided by ‘create-react-app’

Application.test.js preview

To run this test, we will open the terminal and type ‘npm test’ or ‘yarn test’. This command will run the test in watch mode. 

Watch mode in Jest

You can see a lot of options here to run the test. Typing ‘a’ will run all the tests from the application. 

Running our First Test

Our first test is a success! Now let’s move on and understand what a test in React looks like.

Resolve React errors faster with Zipy. Use advanced devtools and AI assistance.

Try for free

Anatomy of a Test in React

In React, a test file ends in either ".test.js" or ".spec.js." The programme locates all the files with these extensions to run when doing testing. Furthermore, our tests file may be identified by putting it inside the '__tests__' subdirectory.

To write a test, developers typically utilise a ".test.js" file. This file is stored with the Component JSX file at all times. As a general practice, a folder named after the component is created, and its JSX, test, and CSS files are stored inside.

- - - Components

- - - - - - Component-Name

- - - - - - - - - Component.jsx

- - - - - - - - - Component.test.js

- - - - - - - - - Component.css 

We use a ‘test()’ function to write a test case. The test function consists of three parameters - the name of your test, a testing function, and a timeout for asynchronous tests. The default timeout is 1000ms.

test("name of test",()=>fn(),timeout)

'it()' can also be used to indicate a test case. "test()" and "it()" are interchangeable and perform the same function.Using the'render()' method, we render a component on which we wish to run tests inside of a test case function.

We utilise the Testing library's queries to choose the component's elements. There are two sections to this questions. The search type is the other, and the variant is the first. For instance, we use the query "getByText()" in our sample test case; the variant is "get...," and the search type is "ByText."The six query variations are displayed in the table below.

Variant If found, return If Not Found, return Multiples allowed? Asynchronous Operations
getBy element error no no
queryBy element null no no
findBy element error no yes
getAllBy array error yes no
queryAllBy array null yes no
findAllBy array error yes yes

In summary, the getBy query can be used to retrieve a single element. If the element is missing, however, this query will return an error. Therefore, utilise queryBy when we wish to assert that the element is not present.

Findby and findAllBy should always be used for asynchronous tasks. You can use getAllBy, queryAllBy, and findAllBy to retrieve numerous entries.To locate the elements according to certain criteria, search kinds are utilised. Here are a few typical search categories.

ByRole By the role of the element in DOM node
ByLabelText By text present on label tag
ByPlaceholderText By placeholder text in input tag
ByText By Text present on screen
ByDisplayValue By element matching display value
ByAltText By Alt value passed to the img tag
ByTitle By title attribute
ByTestId By value matching with data-testid attribute


After we query the element which we want, we can assert some statements based on the test cases. A single test case can have multiple assertions. To make assertions we can use ‘expect()’. The queried element is passed as a parameter to the ‘expect()’ function and a method is called which specifies the condition for assertion.

expect(linkElement).toBeInTheDocument();

In the above code, we are expecting the ‘linkElement’ to be present in the Document using the ‘.toBeInTheDocument()’ method. Similar to this method, we have numerous other methods to check if present, if not present, if true, if false, and more. Some common ones are mentioned below.

  • toBeInTheDocument
  • toBeDisabled
  • toBeEnabled
  • toBeInvalid
  • toBeValid
  • toBeVisible
  • toHaveAttribute
  • toHaveClass
  • toHaveFormValues
  • toHaveStyle
  • toHaveTextContent
  • toHaveValue
  • toHaveDisplayValue
  • toBeChecked
  • toHaveDescription

A ‘describe()’ block represents a test suite. A test suite can have one or more Test cases. It is not necessary for your tests to be inside a suite. As a standard practice, all similar test cases are kept inside one Test Suite. 

describe("Name of test suite",()=>{
      test()
      test()
      test()
    })

Now that we have understood what a test in React looks like, it’s now time to write our first test. 

Writing our First test

We will create a component folder that will have all our Components and their test file. Inside the components folder, we will create an Application Folder for the application component. Inside this, we will add an ‘Application.js’ file and an ‘Application.test.js’ file.

Folder Structure of Components and Tests

Let's add some basic HTML inputs and some heading text inside our ‘Application.js’ file to test.      

import React from "react";
export default function Application() {
  return (
   <div>
      <h1>Login Form</h1>
      <form>
        <div>
          <label htmlFor="username">Enter your Username</label>
          <input
            type="text"
            id="username"
            name="username"
            placeholder="Username"
          />
        </div>
        <div>
          <label htmlFor="password">Enter your Password</label>
          <input
            type="password"
            id="password"
            name="password"
            placeholder="Password"
          />
        </div>
        <button>Login</button>
        <label>
          <input type="checkbox"  data-testid='test-checkbox'/>
          Keep Me Signed In.
        </label>
      </form>
    </div>
  );
}

Now let’s move on to the ‘Application.test.js’ file and write our first test to check if the heading “Login Form” is present on the screen

Application.test.js

import { render, screen } from "@testing-library/react";
import Application from "./Application";

test("Login Form Heading present", () => {
  render(<Application />);
  //get by texta
  const headingelement = screen.getByText(/login form/i);
  expect(headingelement).toBeInTheDocument();
});

As you can see in the above code, we have rendered the Application Component, selected the heading element by ‘getByText()’ query and asserted it to be in the document using ‘.toBeInTheDocument()’ method. Before running the test we will delete the sample ‘App.test.js’ file to avoid confusion. Now let’s run the test and check if the test passes. 

Running test for Application.test.js

Voila!! Our first test ran successfully! Just to make sure our test is asserting the text correctly, We will change the heading from ‘Login Form’ to ‘SignUp Form’. Now, let's run the test again. 

Test failed for Application.test.js

Our test did fail! If we check the log, we can find the exact reason why the test failed. This will help in debugging the code once the application grows. 

In some cases, we might not have the exact text which you want to test. In that case, we can make use of regular expressions. 

For example, in the below code login form will be selected and the case of the text will be ignored.  

const headingelement = screen.getByText(/login form/i);

Let’s write another test but this time using ‘getByRole()’ query. 

test("Login Button Present",()=>{
  render(<Application />);
  //get by role
  const loginButton = screen.getByRole("button");
  expect(loginButton).toBeInTheDocument();
})


Test passed for checking Login button

In the above code, we are testing if a Login button is present on the screen. We are selecting the button by the role ‘button’. Every element in HTML has a specified role. For example, <h1>- <h6> tags have heading role, <button> tag has button role, etc. You can find the entire list of roles here.

Let’s write some more tests with variations of the query and run the tests. 

import { render, screen } from "@testing-library/react";
import Application from "./Application";
import "@testing-library/jest-dom";

describe("Application Component Testing", () => {
    test("Login Form Heading Present", () => {
      render(<Application />);
      const headingelement = screen.getByText("Login Form");
      expect(headingelement).toBeInTheDocument();
    });
    test("Button for Login present", () => {
      render(<Application />);
      //get by role
      const loginButton = screen.getByRole("button");
      expect(loginButton).toBeInTheDocument();
    });
    test("Check if Text Box for username and password is present", () => {
      render(<Application />);
      //get by role with parameters
      const usernameInput = screen.getByRole("textbox", {
        name: "Enter your Username",
      });
      expect(usernameInput).toBeInTheDocument();
      //get by placeholder text
      const passwordInput = screen.getByPlaceholderText("Password");
      expect(passwordInput).toBeInTheDocument();
    });
    test("Check if checkbox and label text is present", () => {
      render(<Application />);
      //get by test-id
      const checkbox = screen.getByTestId("test-checkbox");
      expect(checkbox).toBeInTheDocument();
      //get by label text
      const labeltext = screen.getByLabelText(/signed/i);
      expect(labeltext).toBeInTheDocument();
    }); 
});


Running tests for Application.test.js

Resolve React errors faster with Zipy. Use advanced devtools and AI assistance.

Try for free

Testing User Interactions

Up until now we have done tests on the elements present on the screen. In this section, we will test the UI after some user interactions. For this test, we will create a separate folder with the Counter component. 

Counter.js 

import React, { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p data-testid="count">{count}</p>
      <button onClick={(count) => setCount(count + 1)}>Click</button>
    </div>
  );
}

We have added some basic counter logic which will increment the state variable with every click of a button. 

Counter.test.js

import {render, screen } from "@testing-library/react";
import Counter from "./Counter";

test("Check if Initial Count is 0", () => {
  render(<Counter />);
  const countText = screen.getByText("0");
  expect(countText).toBeInTheDocument();
});

test("Check if Button is present", () => {
  render(<Counter />);
  const button = screen.getByRole("button");
  expect(button).toBeInTheDocument();
});

We have added two tests, one which checks if the initial count is zero and other that checks if the button is present on the screen. 

Now let’s write a test that will check if the count is updated once the user clicks on the button. To test such user interactions, we can use ‘fireEvent’ from the ‘@testing-library/react package’

test("Check if Count is incremented", () => {
  render(<Counter />);
  const countText = screen.getByTestId("count");
  const button = screen.getByRole("button");
  fireEvent.click(button);
  expect(countText).toHaveTextContent(1);
});

As shown in the above code, we are selecting a button from the UI and firing a single-click event using fireEvent. Since we click the button once, the initial count will be incremented to 1 and hence the assertion is true and our test is passed successfully!

Test passed for user interactions

Debugging the tests using React Testing Library

Debugging is an important feature and can save a lot of time and effort while bug solving. Fortunately, the @testing-library/react provides us with enough methods to debug our tests. 

We will modify our test from the last section as below. We have added a screen.debug() method after the render method. This method shows an entire DOM structure present in the component. 

test("Check if Count is incremented", () => {
  render(<Counter />);
  screen.debug()
  const countText = screen.getByTestId("count");
  const button = screen.getByRole("button");
  fireEvent.click(button);
  expect(countText).toHaveTextContent(1);
});

Debugging using screen.debug()

Never Commit your debug statement in your code. Always remove the screen.debug() statement. 

Another useful tool for debugging is the Testing Playground Chrome extension. You can use this extension to find if the element is present, and how to target them accurately. Once you install the extension you can open it from Chrome Dev tools. 

Debugging using Testing Playground

Testing Asynchronous Operations

Asynchronous operations take time to finish their execution. Fetching data, sending data, saving data, and waiting for a timer are all examples of Asynchronous Operations. In this section, we will see how we can test components with Asynchronous actions.

We will create a CounterByDelay folder inside the Components Folder. Our‘CounterByDelay.js’ and ‘CounterByDelay.test.js’ files will go here.

CounterByDelay.js

import React,{useState} from "react";

const CounterByDelay = () => {
  const [count, setCount] = useState(0);

  const delayCount = () =>
    setTimeout(() => {
      setCount(count + 1);
    }, 500);

  return (
    <>
      <h1 data-testid="count">{count}</h1>
      <button data-testid="count-button" onClick={delayCount}>
        Count by delay
      </button>
    </>
  );
};

export default CounterByDelay;

In the above code, we have added a ‘delayCount()’ function which sets the count + 1 after a delay of 0.5s. Let’s write a test that waits for the count to update before testing.

import React from "react";
import {render,fireEvent,waitFor,screen} from "@testing-library/react";
import CounterByDelay from "./CounterByDelay";


test("Increment Count after delay", async () => {

  render(<CounterByDelay />);
  fireEvent.click(screen.getByTestId("count-button"));
  const counter = await waitFor(() => screen.getByText("1"));
  expect(counter).toHaveTextContent("1");

});

Here we are using a waitFor() function from @testing-library/react which waits for the count to update. 

Another use case of Asynchronous tests is testing an element that is currently not inside the Component but will eventually be added. 

Mocking and testing HTTP requests

One of the common responsibilities of the UI is to send and receive requests from an API over HTTP protocol. We can write a function to test the API but that would result in a lot of unnecessary API requests. In case your API is billed for every request, you will be billed for all the requests that you sent just for testing. In such cases, we can mock an API and test the response. 

For mocking API while testing, we will use a package called mock-service-worker

Let’s start by installing msw.

npm install msw –save-dev

Once msw is installed, we need to create a component that sends a get request to the API. In our case, We will be creating a Users component which will get 10 users with the help of JSON Placeholder API.

Users.js

import { useState, useEffect } from 'react'

export const Users = () => {
  const [users, setUsers] = useState([])
  const [error, setError] = useState(null)
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then((res) => res.json())
      .then((data) => setUsers(data))
      .catch(() => setError('Error fetching users'))
  }, [])
  return (
    <div>
      <h1>Users</h1>
      {error && <p>{error}</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

We have created a simple Users component which will render a list of users and will show error in case something goes wrong. Now before we start writing tests, we must set up a dummy server and a handler that will handle our requests. 

I am creating a mocks folder in src which will have two files. Let’s add our first file ‘server.js’ here. 

Server.js

// src/mocks/server.js
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers);

You can find this code in the official documentation here

Let's create a second file named handlers.js in the same folder which will handle all our HTTP requests and responses.

handlers.js

import { rest } from "msw";

export const handlers = [
  rest.get("https://jsonplaceholder.typicode.com/users", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        {
            id:1,
            name: "Anurag Gharat",
        },
        {  
            id:2,
            name: "Steve Rogers",
        },
        {  
            id:3,
            name: "Tony Stark",
        },
      ])
    );
  }),
];

This file exports a handler which has a rest.get() function with two parameters. The first one is the API that we want to intercept and the second is the handler that mocks the API. For mocking, we are using an array of 3 users which resembles the response from the JSON Placeholder API.

Our final change is in the ‘setupTests.js’ file. Replace the existing code with the below code.

setupTests.js

// src/setupTests.js
import { server } from "./mocks/server.js";
// Establish API mocking before all tests.
beforeAll(() => server.listen());

// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());

// Clean up after the tests are finished.
afterAll(() => server.close());

In the above code we are setting up a server for all the requests, after every request, we are resetting the server and after all the tests are performed the server gets closed. 

This is what our folder structure looks like 

Folder Structure

Now with all our setup done, let’s write some tests for testing the API. 

test("Shows a list of 3 users", async () => {
    render(<Users />);
    const users = await screen.findAllByRole("listitem");
    expect(users).toHaveLength(3);
  });

In the above code we are testing if the API returns 3 values. The 3 values are the ones that we are sending through the mock server. Let’s run the tests.

Test passed in Mock requests

As you can see, all the tests passed successfully. Just to verify if our test is working, change the value to have a length of anything other than 3. We will change it to 4 and now our test should fail.

Test passed in Mock requests

Great! This means our Mock server is working!

Just as we tested the response of the API, it's always a good practice to test the error handling. If you check the Users.js code I am already handling the Error using setError(). This means in error cases, the component should render “Error while fetching users” instead of the users. Let’s write a test for this scenario.

test("Shows error in case the API fails", async () => {
    server.use(
      rest.get(
        "https://jsonplaceholder.typicode.com/users",
        (req, res, ctx) => {
          return res(ctx.status(500));
        }
      )
    );
    render(<Users />);
    const error = await screen.findByText("Error while fetching users");
    expect(error).toBeInTheDocument();
  });
});

In the above code, we created another test that will test the error. Here inside the test, we are setting up a server that returns an error with status 500. Then we are asserting the Error text to be present on the screen. Let’s test this test case. 

Test passed for Error Handling case

Resolve React errors faster with Zipy. Use advanced devtools and AI assistance.

Try for free

Code Coverage

Code Coverage means how much your code has been executed while running the test. Consider this a kind of report consisting of all our test cases. In order to create a report we must first add a script in our package.json file. Open your package.json file and add the below line in the scripts object. 

"coverage": "yarn test --coverage --watchAll"

The above line will add a coverage script for your project. In order to run the script you can type 

npm run coverage

Or 

yarn coverage

Code coverage of all files

We now have a report on all the files and the test coverage. But if you see closely some unnecessary JS files are also present. We can ignore them by adding an extra flag at the end of the command. 

"coverage": "yarn test --coverage --watchAll --collectCoverageFrom='src/Components/**/*.{js,jsx}'"

Now if we run the command again. A new coverage report will be generated with only the files present in the Components folder. 

Code Coverage of files in component folder 

You can find all the code on my GitHub

Best Practices

Now that we have understood the React Testing Library, let’s see some best practices we should follow. 

  • Try the user interface instead of the implementation specifics: The User Interface is the only thing that is tested using the React Testing Library. All that is tested is the user's interaction with the UI. This library should not be used for testing anything that occurs in the background. 
  • Use getBy, queryBy, and findBy appropriately: Each query carries out a distinct function. If you use one instead of the other, the test might not pass. Use queryBy only, for instance, if your query is capable of returning a null result. The test will fail if getBy is used.
  • One Assertion per Test Case: You should only make one assertion for each test case. This will enable you to test a scenario at a time and speed up troubleshooting in the event that a test fails. 
  • Mock the external dependencies: Any external dependency like an API or local storage data should be mocked. Making unnecessary API requests will increase your load on API and slow down your testing. 
  • Keep your code coverage at 80%: This lowers the number of defects and is usually considered good practice.
  • Avoid creating pointless and repeated tests: Tests for previously covered and tested material should not be written. For instance, you don't need to create a test to determine whether the input is present on screen if you are writing a test to determine whether the input type is a checkbox because this is already covered in the checkbox type check test.

Resolve React errors faster with Zipy. Use advanced devtools and AI assistance.

Try for free

Wrapping up

Unit Testing should never be avoided if you want your application to have minimum errors and defects. The React Testing Library is a great package to test React applications by generating tests that closely resemble user scenarios. 

During debugging React applications it is important to understand where the error occurred or where exactly the customer faced the issue. This is where you can use Zipy and monitor real-time sessions and debug your React code quickly. Zipy.ai combines stack trace and session replay to make it really easy for developers to identify errors and debug them.

Coming back to this blog, we learned about performing Unit Tests in React using React Testing Library and Jest. We wrote some tests and studied their variations. In the end, we saw how we could create a report for all our tests. We hope you found this blog helpful.

Happy Coding!

lack

Wanna try Zipy?

Zipy provides you with full customer visibility without multiple back and forths between Customers, Customer Support and your Engineering teams.

The unified digital experience platform to drive growth with Product Analytics, Error Tracking, and Session Replay in one.

product hunt logo
G2 logoGDPR certificationSOC 2 Type 2
Zipy is GDPR and SOC2 Type II Compliant
© 2024 Zipy Inc. | All rights reserved
with
by folks just like you