React Component Testing: A Complete Guide to Jest and React Testing Library

As a developer we want the chances of our program failing to be minimal, that’s why testing is an important part of modern development. In a react application as the complexity of the application increases, it becomes critical to test the working of the components keeping the end user in mind and how they would use our application.

In this blog we will walk through the basics of React testing, focusing on unit testing and integration testing using popular React testing tools like JEST and React Testing Library.

Why Testing Matters

It’s impractical if we expect an application’s components to work perfectly fine when we perform some modification to the existing code. While doing this things may or may not break but for the performance of our application it is better to assume the worst and work on it. Testing ensures that our application works as expected in all these scenarios, It helps prevent bugs, enhances code quality, and provides confidence in future refactorings.

React, being a component-based library, is particularly well-suited for unit testing. Since each component has defined inputs (props) and outputs (UI changes), testing them in isolation is easy.

Types of Tests in React

Unit Tests

Unit testing is the simplest form of testing, where we focus on testing a particular function, method, or component in isolation to ensure that they function properly. Doing this we ensure that each component or method in our application works fine before we integrate them.

Integration Tests

Integration testing goes beyond testing particular components in isolation, we test how multiple components or parts of our application interact with each other. This form of testing is used to ensure that two or more units/modules can effectively communicate with one another.

End-to-End Tests

End-to-end testing is the testing technique that requires us to test the whole application from start to end in a production-like environment. We run these tests in a simulated setup to emulate the behavior of a user which can be done using tools like Selenium, Cypress etc.

Setting Up Testing Tools

To get started with testing in the React application, the first step is to install all the necessary libraries. The most popular choice for testing React applications is Jest and we also need the React Testing Library both of these come pre-configured if we have created our project using Create React App.

Jest

Jest is a widely used JavaScript testing framework that ensures the accuracy of your codebase. As a test runner, Jest automatically executes all test files within a project and provides a test report in simple language that we can understand.

For jest to identify the test files automatically in our application, we follow the naming convention for test files as [componentName.test.js].

All the test files will be executed when we run the test command defined in the scripts inside the package.json file.

npm test

React Testing Library

React testing library is a powerful tool used to test React applications. The primary goal of RTL is to test the components in a way that closely resembles how users interact with them. This implies that RTL allows us to test the behavior of our React components from the perspective of our app's users, rather than the implementation details of the component. React testing Library renders our application components which we want to test on a simulated DOM using the render() method, which returns an object using which we get access to different elements from the rendered component using different queries provided by RTL.

Creating and Understanding a Test Block

Consider a simple react component called Header, rendering a heading and subheading on the webpage.

import React from 'react'

export const Header = ({subHeading}) => {
  return (
    <header>
        <h1 data-testid="heading" className='heading'>Welcome</h1>
        <p data-testid="subheading" className='subheading'>{subHeading}</p>
    </header>
  )
}

We create a test file for this header component header.test.js and write a simple test case for the header component.

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

describe("Header component test",()=> {
    test("Testing header component heading",()=> {
        render(<Header subHeading={"Free online character and word count tool."} />);
        const heading = screen.getByTestId("heading");
        expect(heading.innerHTML).toBe("Welcome");
    });
});
  • The describe() function provided by jest is used to combine the tests related to components in a single test suit. It takes a description and a function as arguments.

  • The tests are written inside the test()/it() function provided by jest. It takes two parameters a description for the test and a function with the logic for the test.

  • The render() function provided by React testing Library, is used to render the component passed on the virtual dom for testing.

  • screen.getByTestId(): It is a query method provided by React Testing Library (RTL) that allows you to select an element based on its test ID. RTL offers a variety of query methods like:

    • getby: returns an element based on search type or an error.

    • getByAll: returns an element list based on search type or error.

    • queryBy: returns an element or null

    • queryByAll: returns element list or null.

    • findBy: returns a Promise resolve or Promise reject.

    • findByAll: returns a promise <element list> or a promise rejection.

  • These query methods can be combined with different search types to target specific elements within the rendered output. Some of the search types can be:

    • LabelText: Get the element by the label text of the element.

    • placeholder text: Get the element using the placeholder text of the element.

    • Text: Get the element using the text of the element.

    • TestId: Get the element using the test ID defined in the data attribute.

    • Title: Get the element using it’s title.

  • expect(): Part of Jest, this is used to assert that certain conditions are met by the element. Several matchers are provided by Jest to validate.

    • toBe(): Checks for the values strictly.

    • toBetruthy(): It checks that some true value is returned.

    • toBeInTheDocument(): to check that the element is in the document.

    • toHaveLength(): Tests the length of an array or string.

Here in our code, we are accessing the element using the element test ID and then checking if the value of the element is equal to the value provided in the matcher.

We can run this test using the npm test command. And if the test passes or fails we get the info on our terminal.

Common Testing Patterns

Testing Component Props

We can check the props passed to a component are working fine and result in an expected UI output. For our example, since we are passing the subHeading prop to the header, we can test it.

test("Testing header component subheading",() => {
    render(<Header subHeading={"Understanding React Testing"} />);
    const subheading = screen.getByTestId("subheading");
    expect(subheading.innerHTML).toBe("Understanding React Testing");
});

Here, we have to take care that when we are rendering the component using the render method we should pass the props here also. Then we access the element using the test ID and check its value using the toBe() matcher.

While using the toBe() matcher we should know that it checks the values strictly which means even if there is any extra space or there is a difference in the case of letters in value our test will fail.

Snapshot Testing

Jest provides us the functionality to create snapshots, which is the serialized version of the rendered components. This becomes useful when we want to monitor that UI doesn’t change unexpectedly.

import { render } from '@testing-library/react';
import { Header } from "../Header";

test('matches snapshot', () => {
  const { asFragment } = render(<Header subHeading={"Free online character and word count tool."} />);
  expect(asFragment()).toMatchSnapshot();

});

When the test is run for the first time, a snapshot is created. On subsequent runs, the current output is compared to the snapshot. If there are any differences, the test will fail.

Testing Asynchronous Behavior

When we have asynchronous operations inside our components and if we want to test it then the test also requires to be asynchronous so that the test also waits till the operation is completed before running the test case.

Consider a component Joke inside which we are using an asynchronous function to fetch jokes using an API call to a URL.

import { useEffect, useState} from 'react';

export const Joke = () => {
  const [joke,setJoke] = useState({});
    useEffect(() => {
        async function getJoke(){
            const response = await fetch(apiUrl);
            const data = await response.json();
            setJoke(data);
        };
        getJoke();
    },[])

  return (
    <p className="joke">
      <span data-Testid = "jokeValue">{joke.value} </span>
    </p>
  )
}

There are two ways provided by Jest to write test cases for asynchronous operations.

  • Using Promises: We can return the promise from our test then Jest will wait till the promise is resolved. If the promise is rejected the test will automatically fail.

      test('Testing fetching of jokes', () => {
          return getJoke().then(data => {
            expect(data).toBeTruthy();
          });
      });
    
  • Using async await: We can use an async function in the test and use await to let the test wait for the response.

      import { screen, render, waitFor } from "@testing-library/react";
      import { Joke } from "../Joke";
    
      test("Testing fetching of jokes",async() => {
          render(<Joke />);
          const jokeValue = screen.getByTestId("jokeValue");
          await waitFor(() => expect(jokeValue.innerHTML).toBeTruthy());
      })
    

    waitFor() function is to wait for asynchronous elements to appear or change in the DOM.

User Events Testing

userEvent is a utility provided by React Testing Library to simulate real user interactions such as typing, clicking, and navigating. It is designed to mimic how users interact with your application, making your tests more accurate and reliable compared to using fireEvent. While fireEvent simply triggers an event, userEvent simulates the actual behavior of users in a more natural way.

Syntax:

userEvent.[type-of-event]([element on which event performed])

Consider a Counter component that renders a text area inside which the user can write something and the number of words and characters are counted and shown on the page using span.

import { useRef, useState } from "react";

export const Counter = () => {

    const [charCount, setCharCount] = useState(0);
    const [wordCount, setWordCount] = useState(0);
    const textData = useRef();

    const handleCount = () => {
        const value = textData.current.value;
        setCharCount(value.length);
        setWordCount(value.length ? value.trim().split(" ").length : 0);
    };

    const handleClear = () => {
        textData.current.value = "";
        handleCount();
    };

    return (
        <section className="counter">
            <textarea onChange={handleCount} ref={textData} data-testId="textArea" className="text" placeholder="Type or paste your text" spellCheck="false"></textarea>
            <button data-testId="clearBtn" onClick={handleClear} className="clear" disabled={`${charCount ? "" : "disabled"}`}>Clear</button>
            <div className="counts">
                <span data-testId="charCount" className="character">Character: {charCount}</span>
                <span data-testId="wordCount" className="word">Word: {wordCount}</span>
            </div>
        </section>
    )
}

Now, we will test if we get the correct outputs in render when the user starts typing in the text area using the userEvent.

import { screen, render, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import '@testing-library/jest-dom';
import { Counter } from "../Counter";

describe("Counter component test",()=> {
    test("text entered in textarea", ()=>{
        render(<Counter />);
        const textarea = screen.getByTestId("textArea");
        const charCount = screen.getByTestId("charCount");
        const wordCount = screen.getByTestId("wordCount");

        userEvent.type(textarea, "Abhishek");
        expect(charCount.innerHTML).toBe("Character: 8");
        expect(wordCount.innerHTML).toBe("Word: 1");
    })
});

We get hold of the textArea, the wordCount span, and the CharCount span. We also declare the userEvent with the type of event, and the element on which the event will be performed, and since in our case we are checking the typing event in textArea we will also pass the dummy text that we consider written inside the textArea. Based on this text we will run the test to check the values of wordCount and charCount.

Conclusion

React testing plays a vital role in building reliable, scalable applications. With tools like Jest and React Testing Library, you can write tests that are not only effective but also maintainable and user-centric. By testing components as users interact with them, you ensure that your application functions as expected in real-world scenarios.

Testing may seem like a daunting task, but with practice and the right tools, it becomes an integral part of delivering high-quality React applications.

11
Subscribe to my newsletter

Read articles from Abhishek Sadhwani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abhishek Sadhwani
Abhishek Sadhwani