Seamless Digital Experience.
Happy Customers.

Digital Experience and Error Monitoring Platform - Zipy

How to Write Unit Tests for React Apps?

Anurag Gharat
~ 18 min read | Published on Jan 29, 2024





TABLE OF CONTENT

Fix bugs faster with Zipy!

  • Session replay
  • Network calls
  • Console Logs
  • Stack traces
  • User identification
Get Started for Free

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

Testing in software development is a process of validating and verifying the behavior of the software before it goes live for users. Any unexpected behavior or error encountered during this process is resolved and fixed. Testing can be performed manually or a developer can automate it by writing tests that run the application to find the errors during execution. 

Common types of testing include Unit Testing, End to End Testing, Integration Testing, Acceptance Testing, and more. But the one which is important for our blog is Unit Testing. 

What is Unit Testing?

Unit Testing is a type of testing where every individual element of an application is tested in isolation. Any dependencies that are required for that component are mocked. 

The main purpose of writing a unit test is to find out if a component behaves as it is expected and meets the requirements. Any error identified in this stage is crucial because it saves the time required for debugging later.

As we discussed earlier, everything in React is made up of components. Each component has it own state and performs some specific action on the UI. Failure or error in one component will cause side effects on its child components. Hence testing the behavior of each component in isolation is necessary. React Testing Library does just that!

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

Now that we have understood Unit Tests in React, let’s move on to our next section and write some tests. 

Going into this demo, we expect you to have a prior understanding of React. If you are new to React, we highly suggest you learn and get comfortable with it before you move to testing. 

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

The reason why we are using ‘create-react-app’ is that it comes pre-configured with React Testing Library and Jest. It even has a sample Test case written for us. 

There are other ways of creating a react app but in those cases, we would need to install the Testing packages separately. If you set up your application on your own make sure you install the React Testing Library and Jest. 

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

A test file in react ends with ‘.test.js’ or ‘.spec.js’. When running tests, the application finds all the files with these extensions to run. Additionally, we can also denote our tests file by placing them inside a ‘__tests__’ folder. 

Mostly developers use a ‘.test.js’ file to write a test. This file is always kept alongside the Component JSX file. A common convention is creating a folder by the name of the component and keeping the JSX, test, and CSS files of that component inside the folder. 

- - - 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)

A test case can also be denoted using ‘it()’. Both ‘test()’ and ‘it()’ are the same and do the exact same thing. 

Inside a test case function, we render a component on which we want to perform tests using the ‘render()' method. For selecting the elements from the component, we use queries provided by the Testing library. These queries consist of two parts. One is the variant and the other is the search type. For example in our sample test case we use a query ‘getByText()’, here ‘get...’ is the variant, and ‘ByText’ is the search type. 

The below table shows the 6 variants of the query. 

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

To summarize, when we want to get a single element we can use the getBy query. But this query will give an error if the element is absent. Hence in cases where we want to assert that the element is not present, use queryBy. For asynchronous operations always use findby and findAllBy. For getting multiple elements, use getAllBy, queryAllBy, and findAllBy. 

Search types are used to find the elements based on some criteria. Below are some common search types. 

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. 

  • Test the UI and not the Implementation details: React Testing Library is strictly used for testing the User Interface. It only tests how the user interacts with the UI. Whatever happens behind the scene should not be tested using this library. 
  • Proper use of getBy, queryBy, and findBy: All the queries perform different operations. Using one in place of another can cause the test to fail. For example, Only use queryBy if your query can return a null response. Using getBy will cause the test to fail. 
  • One Assertion for One Test: Limit your assertion to one assertion per test case. This will help you test one scenario at a time and debugging will be faster in case the 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. 
  • Maintain a Code coverage of 80%: Having a code coverage of 80% is generally good practice and reduces the number of bugs. 
  • Don’t write repetitive and unnecessary tests: Avoid writing tests for elements that are already covered and tested. For example: If you are writing a test to check if the input type is a checkbox then you don't have to write a test that checks if the input is present on screen since it 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

Call to Action

Feel free to comment or write to us in case you have any further questions at support@zipy.ai. We would be happy to help you. In case you want to explore for your app, you can sign up or book a demo.











Fix bugs faster with Zipy!

Get Started for Free
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Want to solve customer bugs even before they're reported?

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

SOC 2 Type 2
Zipy is GDPR and SOC2 Type II Compliant
© 2023 Zipy Inc. | All rights reserved
with
by folks just like you