— by Anurag Gharat
React is a front-end JavaScript library built by Facebook for creating user interfaces for websites. According to the Stackoverflow 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 the 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 testing means.
Table of Contents:
Testing
- What is Unit Testing?
What is the React Testing Library?
Writing Unit Tests in React
- Setting up the Environment
- Running the Test
- Anatomy of a Test in React
- Writing our First test
- Testing User Interactions
- Debugging the tests using React Testing Library
- Testing Asynchronous Operations
- Mocking and testing HTTP requests
- Code Coverage
Best Practices
Wrapping Up
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 errors during execution.
Common types of testing include Unit Testing, End to End Testing, Integration Testing, Acceptance Testing, and more. But the one that 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 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 its 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!
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 Airbnb 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 to find the solution as long as 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 they 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’.
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.
You can see a lot of options here to run the test. Typing ‘a’ will run all the tests from the application.
Our first test is a success! Now let’s move on and understand what a test in React looks like.
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.
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.
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.
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.
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.
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.
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();
})
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();
});
});
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 the 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 fire 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!
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);
});
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.
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.
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 errors 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 that 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
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.
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.
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.
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
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.
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.
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 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!
Originally published at https://www.zipy.ai