Beginner Guide on Unit Testing in React using React Testing Library and Vitest

isiakaabd

ISIAKA ABDULAHI AKINKUNMI

Posted on December 18, 2022

Beginner Guide on Unit Testing in React using React Testing Library and Vitest

As Application grows bigger and more complex, manual testing becomes time-consuming, difficult to achieve its purpose and prone to error as it might be difficult to notice the wee change that can break the functionalities. In this article, I'd like to explain the concept of unit testing, I expect you have a basic understanding of React concepts like components and state.

lets get started💧

Table Of Content

What is Unit Testing?

Unit tests are tests focused on individual component, function modules called unit. It isolates the particular unit and test it separately to ensure it works as expected. For React components, it could mean checking if the component renders correctly for a specified prop.

The more your tests resemble the way your software is used, the more confidence they can give you.

Purpose Of Unit Testing

  1. Improved code quality and reliability: Unit testing ensures that individual components and functions in a React application are working as intended, reducing the chances of bugs and errors.

  2. Easier debugging and maintenance: With unit tests in place, it is easier to identify and fix any code issues and make modifications and updates without breaking the application.

  3. Better collaboration and communication among team members: Unit tests provide clear documentation of each component's expected behavior and functionality, enabling team members to understand and work on the code more effectively.

  4. Enhanced user experience: By thoroughly testing each component and function, unit tests ensure that the user experience is consistent and seamless, resulting in a better overall product.

  5. Faster development and deployment: With the ability to quickly identify and fix issues, unit testing allows for faster development and deployment of React applications.

What is React Testing Library?

React Testing Library RTL is a lightweight solution for testing web pages by querying and interacting with DOM nodes.
RTL is a popular testing library for React applications that helps developers write reliable tests for their components. It is designed to encourage writing tests that are easy to read and understand, which makes it a great tool for teams working on large React projects.

One of the key features of react-testing-library is its focus on testing the behavior of a component rather than its implementation. This means that tests written with react-testing-library are less likely to break when the implementation of a component changes, which can save developers a lot of time and effort.
The library provides a set of utility functions for interacting with React components in a way that simulates how a user would interact with them.

Setting React App With Vite

To set up our application, we will be using vite as an alternative to create-react-app, Vite is a lightweight and minimalistic tool, providing only the essential features and tools needed for React development.

To create a vite project, run the following command in your terminal

npm create vite@latest

OR

yarn create vite

Enter fullscreen mode Exit fullscreen mode

Then follow the prompts to input project name, framework and variant:

  • For project name, we can use react-testing-project
  • For framework, select React option
  • For variant select javascript from the available options

After All these, run the following


cd react-testing-project
npm install

OR

cd react-testing-project
yarn install

Enter fullscreen mode Exit fullscreen mode

After these steps, open it in your favorite IDE or run
code . to open in default IDE and start the server with the code below

npm run dev

OR

yarn run dev

Enter fullscreen mode Exit fullscreen mode

Open the URL in your browser and it should display something similar here

image of VS Code

Vitest

Vitest is a blazing-fast unit test framework powered by vite. It aims to position itself as the rest runner of choice for vite projects, and as a solid alternative even for projects not using vite.

Why use Vitest?

  • Shared configuration with vite.
  • Vitest supports HMR (Hot Module Reloading), which speeds up your workflow. With HMR, only the changes are updated on the server, and the server reflects the new changes.
  • Vitest is relatively simple to use and doesn't require complex configuration. This makes it easier to get started with testing your components.
  • Jest Snapshot support.

Stages of Testing

  • Setup: Before you can test an element, you will need to set up your testing environment. This typically involves installing the testing library and any dependencies, creating a test file, and importing the necessary components and utilities from the library.

  • Rendering: To test an element, you will need to render it in a testing environment. You can use the render function from the testing library to do this, which will return an object containing the rendered element and various utility functions for interacting with it.

  • Querying: Once you have rendered the element, you can use the query functions provided by the testing library (such as getByText or getByLabelText) to locate specific elements within the rendered tree.

  • Interacting: After you have located the element you want to test, you can use the utility functions provided by the testing library (such as fireEvent or userEvent) to simulate user interactions with the element.

  • Assertion: Finally, you can use the assertion functions provided by the testing library (such as expect or assert) to verify that the element is behaving as expected in response to the simulated interactions.

    • Setup

Install vitest and jsdom as dev dependencies using:


npm i -D vitest jsdom

OR

yarn add -D vitest jsdom

Enter fullscreen mode Exit fullscreen mode

Also the following also

npm i @testing-library/jest-dom @testing-library/react

OR

yarn add @testing-library/jest-dom @testing-library/react

Enter fullscreen mode Exit fullscreen mode

After all installations are successful, we need to add a test script in our package.json file like this:

test: "vitest"

Enter fullscreen mode Exit fullscreen mode

So, our package.json file, look like this

Package.json file

In src folder, create a setupTests.js and add the following code

import "@testing-library/jest-dom/extend-expect

Also in our vite.config.js add a test object to the config like below


 test: {
    globals: true,
    environment: "jsdom",
    setupFiles: "src/setupTests.js",
  },

Enter fullscreen mode Exit fullscreen mode

so, our vite.config.js looks like this:

vite.config.js file

We are done with installation and configuration, last thing left before running our test is to create __tests__ folder in src folder. we will also create App.test.js in __tests_ folder where we are to write all our codes, our file structure should look like this

file layout

  • Querying

Before writing our test, we should get acquainted with several methods that allow us to locate elements based on their text content, display properties, or other attributes on the page.

  • To select a single element, you can use the getBy*, findBy*, or queryBy*.

  • To select multiple elements, you can use the getAllBy*, findAllBy* or queryAllBy*.

  • queryBy*: These element selectors allow you to search for an element in your component, but don't expect the element to be present. They will return the element if it is found or return null if the element is not found. These element selectors are useful when you want to check for the presence of an element, but don't want the query to throw an error if the element is not found.

  • getBy*: These element selectors allow you to synchronously search for an element in your component. They will return the element if it is found or throw an error if the element is not found. These element selectors are useful when you expect the element to be present in the DOM at the time the query is executed.

  • findBy*: These element selectors allow you to asynchronously search for an element in your component. They will return a promise that resolves to the element when it becomes available or rejects it with an error if the element is not found. These element selectors are useful when you need to wait for an element to appear in the DOM before interacting with it.

  • queryAllBy*: These element selectors allow you to search for multiple elements in your component, but don't expect the elements to be present. They will return an array of elements if any elements are found, or return an empty array if no elements are found. These element selectors are useful when you want to check for the presence of multiple elements, but don't want the query to throw an error if no elements are found.

  • getAllBy*: These element selectors allow you to synchronously search for multiple elements in your component. They will return an array of elements if any elements are found, or throw an error if no elements are found. These element selectors are useful when you expect multiple elements to be present in the DOM at the time the query is executed.

  • findAllBy*: These element selectors allow you to asynchronously search for multiple elements in your component. They will return a promise that resolves to an array of elements when the elements become available, or rejects with an error if no elements are found. These element selectors are useful when you need to wait for multiple elements to appear in the DOM before interacting with them.

Read more about the configuration here

Write Your First Unit Test

In Our App.jsx file, we have some code there that we can write a basic test to debug our screen and check if some elements are present ,

  • Rendering

lets debug our screen, copy and paste the code below

import App from "../App";
import { it, describe } from "vitest";
import { render, screen } from "@testing-library/react";


describe("App.js", () => {
  it("Check if the App render very well", () => {
    //render our App properly
    render(<App />);
    screen.debug();
  });
});

Enter fullscreen mode Exit fullscreen mode

Here, we import our <App/> which is to be tested, Also screen and render are imported to be able to interact with the component. describe method is used to organize and structure tests, and it typically takes a string parameter that describes the group of tests being defined and a callback function that contains the actual tests. In this example, the string argument, "Check if the App render very well", provides a label for the group of tests, and the callback function contains the actual tests that are being run. The render method is used to render a given React component and return an object that provides several utility functions for interacting with the rendered component. screen.debug method is a utility function that allows you to print the current state of the rendered component to the console. This can be useful when writing or debugging tests.

lets run the test by entering the following code

npm run test

OR

yarn run test

Enter fullscreen mode Exit fullscreen mode

After running the test, it renders our <App/> in the terminal as shown below

result of test

  • Assertion lets check if button and heading elements are in our component will the following code:
import App from "../App";
import { it, describe } from "vitest";
import { render, screen } from "@testing-library/react";

describe("App.js", () => {
  it("Check if the button is in the document", () => {
    render(<App />);
    const button = screen.getByRole("button");
    expect(button).toBeInTheDocument();
  });

  it("check if h1 element is in the document", () => {
    render(<App />);
    const h1 = screen.getByRole("heading", { level: 1 });
    expect(h1).toBeInTheDocument();
  });
});

Enter fullscreen mode Exit fullscreen mode

Here, we are having two tests that render our <App/> , in App.jsx , I added an attribute of role with the value of button to easily find the element, getByRole method accepts a second parameter which we used to indicate the heading is h1. expect is used to define the expected behaviour of a test and to verify that the actual behaviour matches the expected behaviour. in our two tests, we expect both elements to be in the document. below is what happens when we run our test

expected result on test

  • Interaction In this test, we will test when a user interact with element such as click event and check the state change as shown below

import App from "../App";
import { it, describe,  } from "vitest";
import { render, screen } from "@testing-library/react";

describe("App.js", () => {
  it("Check if the button is in the document", () => {
    render(<App />);
    const button = screen.getByRole("button");
    expect(button).toBeInTheDocument();
  });

  it("check if h1 element is in the document", () => {
    render(<App />);
    const h1 = screen.getByRole("heading", { level: 1 });
    expect(h1).toBeInTheDocument();
  });
  it("Check if the current value renders when click", () => {
    render(<App />);
    const h1 = screen.getByRole("heading", { level: 1 });
    expect(h1.textContent).toBe("0");
  });
});

Enter fullscreen mode Exit fullscreen mode

since we render App.jsx in the three test, we can refactor our code and keep it DRY by using another method beforeEach, which runs before each test, so our updated test code becomes

import App from "../App";
import { it, describe, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";

beforeEach(() => {
  render(<App />);
});

describe("App.js", () => {
  it("Check if the button is in the document", () => {
    const button = screen.getByRole("button");
    expect(button).toBeInTheDocument();
  });

  it("check if h1 element is in the document", () => {
    const h1 = screen.getByRole("heading", { level: 1 });
    expect(h1).toBeInTheDocument();
  });
  it("Check if the current value renders when click", () => {
    const h1 = screen.getByRole("heading", { level: 1 });
    expect(h1.textContent).toBe("0");
  });
});

Enter fullscreen mode Exit fullscreen mode

With this, Our code becomes neater and more readable, our test is expected to run successfully.

I made a change to App.jsx file, by putting our state in the h1 element. as shown below

updated App.js file

To simulate the click event, I added the following code

it("Check if the  value increases when button is clicked", async () => {
    const button = screen.getByRole("button");
    await userEvent.click(button);
    const h1 = screen.queryByRole("heading", { level: 1 });
    expect(h1.textContent).toBe("4");
  });

Enter fullscreen mode Exit fullscreen mode

we use async and await when testing asynchronous events such as click, because it ensures that the test will wait for the click event to complete before moving on to the next step. running this test will definitely fail because the expected value when clicked is 1 instead of 4

failed test result

So if we change the value from 4 to 1, the test runs as expected as shown below

successfully test

Thank you for reading. I hope you've learned something new from this post. The code can be found in this repository Want to stay up to date with regular content regarding JavaScript, React? Follow me on LinkedIn.

💖 💪 🙅 🚩
isiakaabd
ISIAKA ABDULAHI AKINKUNMI

Posted on December 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related