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 ?
- Purpose Of Unit Testing
- What is React Testing Library ?
- Setting React App With Vite
- Vitest
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
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.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.
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.Enhanced user experience: By thoroughly testing each
component
andfunction
,unit tests
ensure that the user experience is consistent and seamless, resulting in a better overall product.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
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
selectjavascript
from the available options
After All these, run the following
cd react-testing-project
npm install
OR
cd react-testing-project
yarn install
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
Open the URL in your browser and it should display something similar here
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
supportsHMR
(Hot Module Reloading), which speeds up your workflow. WithHMR
, 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
orgetByLabelText
) 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
oruserEvent
) 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
Also the following also
npm i @testing-library/jest-dom @testing-library/react
OR
yarn add @testing-library/jest-dom @testing-library/react
After all installations are successful, we need to add a test script in our package.json
file like this:
test: "vitest"
So, our package.json
file, look like this
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",
},
so, our vite.config.js
looks like this:
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
- 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*
, orqueryBy*
.To select multiple elements, you can use the
getAllBy*
,findAllBy*
orqueryAllBy*
.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();
});
});
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
After running the test, it renders our <App/>
in the terminal as shown below
-
Assertion
lets check if
button
andheading
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();
});
});
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
-
Interaction
In this test, we will test when a user interact with element such as
click
event and check thestate
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");
});
});
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");
});
});
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
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");
});
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
So if we change the value from 4
to 1
, the test runs as expected as shown below
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.
Top comments (0)