A Guide to Testing React Components with Jest and React Testing Library
Table of contents
- Importance of Testing in React Development
- Introduction React Testing Library
- Introduction to Jest
- Setting Jest and React Testing Environment
- Understanding Jest Syntax
- Let's start writing testcases
- Debugging and Finding Errors Using Test Cases
- Conclusion
- FAQs
- 1. What is the difference between Jest and React Testing Library?
- 2. How do I mock API calls in Jest when testing React components?
- 3. What is the best approach to test asynchronous code in React components?
- 4. How do I simulate user events like clicks or input changes in React Testing Library?
- 5. Why is it important to use screen for querying elements in React Testing Library?
Testing is checking if your code works the way it's supposed to. When you write a program, you have an idea of what it should do. Testing is the process of making sure it does that. It's like double-checking your work.
In this article, we're diving into the world of React testing using two powerful tools: Jest and React Testing Library. We'll explore how these tools work together to create robust, reliable tests for your React applications.
Importance of Testing in React Development
Testing is a crucial step in the development process of React-based applications, similar to any other software development.
React uses a component-based structure, meaning a single change in one component could potentially affect others. This interdependence is why testing is so important in React. In case a component fails the test, you will be notified immediately, allowing for quick fixes and minimizing work stoppage.
Introduction React Testing Library
React Testing Library is a helpful tool for testing React components. It helps you test your React components in a way that's close to how a user would interact with them.
It is built top of DOM Testing Library
, which is is a very light-weight solution for testing DOM nodes (whether simulated with JSDOM
as provided by default with Jest or in the browser). It creates a simulated web page (a DOM) for each of your components, which involve querying the DOM for nodes in a way that's similar to how the user finds elements on the page.
Introduction to Jest
Jest is a complete and easy-to-set-up JavaScript testing solution. It's created and maintained by Facebook.
It includes Test runner , Assertion library and Mocking features. Jest provides a way to check if values meet certain conditions with Assertion library. Mocking allows jest to create fake version of functions or modules for testing.
Setting Jest and React Testing Environment
In this section will setup a simple react app with create-react-app
for our testing environment using Jest and React Testing library. You can choose vite
or any other react framework.
Feel free to clone the sandbox below and follow along
Setting up CRA
npx create-react-app jest-testing-blog
cd jest-testing-blog
CRA comes pre configured with jest testing environment, for other framework we might need to install it manually.
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
Filename Conventions
Jest will look for test files with any of the following popular naming conventions:
Files with
.js
suffix in__tests__
folders.Files with
.test.js
suffix.Files with
.spec.js
suffix.
The .test.js
/ .spec.js
files (or the __tests__
folders) can be located at any depth under the src
top level folder.
For larger projects it's recommends to put all
.test.js
/.spec.js
files in__tests__
folders undersrc
folder
Project Overview
To demonstrate Jest and React Testing Library, we would be using a simple Todo application.
Features of the Todo app:
Add new todo items
Mark todo items as complete
Delete todo items
Filter todos (All, Active, Completed)
This project is small enough to be manageable but includes enough functionality to showcase various testing scenarios.
The app will include the following components:
TodoList: The main component that holds the state and renders other components
TodoItem: Represents a single todo item
AddTodo: A form to add new todos
FilterTodos: Buttons to filter the todo list
Understanding Jest Syntax
Jest provides a simple structure for writing testcases.
describe('Group of tests', () => {
test('individual test', () => {
// Test code
});
});
describe
: Groups related tests together. Useful when we are doing integration testing which consist of multiple unit testing.test
: Defines an individual test case.
Jest provides various assertion methods to check if values meet certain conditions:
expect(value).toBe(expectedValue);
expect(value).toEqual(expectedValue);
expect(value).toBeTruthy();
expect(array).toContain(item);
Jest also allows to run functions before and after executing test cases.
beforeEach(() => {
// Runs before each test in this file
});
afterEach(() => {
// Runs after each test in this file
});
Jest also support asynchronous code
test('async test', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
Writing your first jest test case
Let's write a simple jest test for your Counter component using Jest and React Testing Library. This test will check if the component renders correctly and if the increment and decrement buttons work as expected.
Create a Counter.test.js
under src/components/__test__
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './../Counter.js';
describe('Counter Component', () => {
// First Test Case
test('renders initial count of 0', () => {
render(<Counter />);
const countElement = screen.getByText(/Counter: 0/i);
expect(countElement).toBeInTheDocument();
});
// Second Test Case
test('increments count when increment button is clicked', () => {
render(<Counter />);
const incrementButton = screen.getByText(/Increment/i);
fireEvent.click(incrementButton);
const countElement = screen.getByText(/Counter: 1/i);
expect(countElement).toBeInTheDocument();
});
// Third Test Case
test('decrements count when decrement button is clicked', () => {
render(<Counter />);
const decrementButton = screen.getByText(/Decrement/i);
fireEvent.click(decrementButton);
const countElement = screen.getByText(/Counter: -1/i);
expect(countElement).toBeInTheDocument();
});
// Fourth Test Case
test('correctly updates count with multiple clicks', () => {
render(<Counter />);
const incrementButton = screen.getByText(/Increment/i);
const decrementButton = screen.getByText(/Decrement/i);
fireEvent.click(incrementButton);
fireEvent.click(incrementButton);
fireEvent.click(decrementButton);
const countElement = screen.getByText(/Counter: 1/i);
expect(countElement).toBeInTheDocument();
});
});
This jest test case file does the following:
It imports the necessary testing utilities from react testing and the Counter component.
Jest uses a
describe
block to group related jest tests for the Counter component.The first jest test case checks if the initial count is rendered as 0.
The second testcase clicks the increment button and checks if the count increases to 1.
The third test clicks the decrement button and checks if the count decreases to -1.
The fourth test performs multiple clicks on both buttons and checks if the final count is correct.
We can usually run the testcases using jest with:
npm test
which test all our testcases and return something as below:
PASS src/components/__test__/Counter.test.js
Counter Component
√ renders initial count of 0 (7 ms)
√ increments count when increment button is clicked (3 ms)
√ decrements count when decrement button is clicked (3 ms)
√ correctly updates count with multiple clicks (3 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 0.451 s, estimated 1 s
Ran all test suites related to changed files.
Now as we know how jest and its syntax works, let's move into writing test cases for our simple todo application.
Let's start writing testcases
TodoList
Create a TodoList.test.js
under src/components/__test__
This test file contains four test cases for a TodoList component. Let's break down what each test case does:
Test: adds a new todo
Finds the input field and "Add" button
Simulates typing "New todo" into the input field
Simulates clicking the "Add" button
Checks if "New todo" appears in the document, confirming the todo was added
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import TodoList from './../TodoList.js';
test('adds a new todo', () => {
render(<TodoList />);
const input = screen.getByPlaceholderText('Add a new todo');
const button = screen.getByText('Add');
fireEvent.change(input, { target: { value: 'New todo' } });
fireEvent.click(button);
expect(screen.getByText('New todo')).toBeInTheDocument();
});
Test: marks a todo as complete
Finds the checkbox associated with the new todo
Simulates clicking the checkbox
Checks if the checkbox is now checked, confirming the todo was marked as complete
test('marks a todo as complete', () => {
render(<TodoList />);
const input = screen.getByPlaceholderText('Add a new todo');
const addButton = screen.getByText('Add');
fireEvent.change(input, { target: { value: 'New todo' } });
fireEvent.click(addButton);
const checkbox = screen.getByRole('checkbox');
fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
});
Test: deletes a todo
Finds the "Delete" button associated with the new todo
Simulates clicking the delete button
Checks if "New todo" is no longer in the document, confirming the todo was deleted
test('deletes a todo', () => {
render(<TodoList />);
const input = screen.getByPlaceholderText('Add a new todo');
const addButton = screen.getByText('Add');
fireEvent.change(input, { target: { value: 'New todo' } });
fireEvent.click(addButton);
const deleteButton = screen.getByText('Delete');
fireEvent.click(deleteButton);
expect(screen.queryByText('New todo')).not.toBeInTheDocument();
});
Test: filters todos
Adds two todo items: "Todo 1" and "Todo 2"
Marks "Todo 1" as complete by clicking its checkbox
Finds and clicks the "Active" filter button
Checks if "Todo 1" (completed) is no longer visible
Checks if "Todo 2" (active) is still visible
test('filters todos', () => {
render(<TodoList />);
const input = screen.getByPlaceholderText('Add a new todo');
const addButton = screen.getByText('Add');
fireEvent.change(input, { target: { value: 'Todo 1' } });
fireEvent.click(addButton);
fireEvent.change(input, { target: { value: 'Todo 2' } });
fireEvent.click(addButton);
const firstCheckbox = screen.getAllByRole('checkbox')[0];
fireEvent.click(firstCheckbox);
const activeFilterButton = screen.getByText('Active');
fireEvent.click(activeFilterButton);
expect(screen.queryByText('Todo 1')).not.toBeInTheDocument();
expect(screen.getByText('Todo 2')).toBeInTheDocument();
});
These tests cover the main functionality of the TodoList component:
Adding new todos
Marking todos as complete
Deleting todos
Filtering todos based on their status (active/completed)
we can also have multiple test files for each separate component inside TodoList
.
AddTodo
Create a AddTodos.test.js
under src/components/__test__
for testing the addtodo component
Form Submit
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import AddTodo from './../AddTodo.js';
test('calls addTodo prop with input value when form is submitted', () => {
const mockAddTodo = jest.fn();
render(<AddTodo addTodo={mockAddTodo} />);
const input = screen.getByPlaceholderText('Add a new todo');
const button = screen.getByText('Add');
fireEvent.change(input, { target: { value: 'New todo' } });
fireEvent.click(button);
expect(mockAddTodo).toHaveBeenCalledWith('New todo');
});
Input clear after submission
test('clears input after form submission', () => {
render(<AddTodo addTodo={() => { }} />);
const input = screen.getByPlaceholderText('Add a new todo');
const button = screen.getByText('Add');
fireEvent.change(input, { target: { value: 'New todo' } });
fireEvent.click(button);
expect(input.value).toBe('');
});
FilterTodo
Create a FilterTodos.test.js
under src/components/__test__
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import FilterTodos from './../FilterTodos.js';
test('calls setFilter with correct argument when buttons are clicked', () => {
const mockSetFilter = jest.fn();
render(<FilterTodos setFilter={mockSetFilter} />);
fireEvent.click(screen.getByText('All'));
expect(mockSetFilter).toHaveBeenCalledWith('all');
fireEvent.click(screen.getByText('Active'));
expect(mockSetFilter).toHaveBeenCalledWith('active');
fireEvent.click(screen.getByText('Completed'));
expect(mockSetFilter).toHaveBeenCalledWith('completed');
});
To run the test cases we've created, just run npm test
in the terminal.
Oops! Something went wrong as one of the testcase failed under TodoList.test.js
Debugging and Finding Errors Using Test Cases
One of the main benefits of writing test cases is that they help us catch errors early in the development process. When a testcase fails, it provides valuable insights about where and why the failure occurred, allowing you to quickly diagnose and fix the problem.
There is a subtle bug into the
AddTodo
component that will cause one of the test cases to fail.
Fixing Error
In the handleSubmit
function, the line that clears the input field (setText('')
) after submitting the form has been commented out. This will cause the input field to retain the text even after the form is submitted.
modify AddTodo.js
file under components
import React, { useState } from 'react';
function AddTodo({ addTodo }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
addTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
}
export default AddTodo;
Running the Tests
Hurray 🎉! Our tests are running fine.
Conclusion
In this article, we explore React testing using Jest and React Testing Library. We introduce the importance of testing in React development, set up a React testing environment, and explain essential Jest concepts and syntax.
Testing isn't just a checkbox on a to-do list; it's a crucial aspect of modern software development. By adopting a thorough testing strategy with tools like Jest and React Testing Library, we’re setting the foundation for a more robust, reliable, and maintainable React application.
FAQs
1. What is the difference between Jest and React Testing Library?
Jest is a JavaScript testing framework designed for unit testing, while React Testing Library is a utility to test React components by simulating user interactions. Jest provides testing infrastructure, including a test runner, assertion library, and mocking capabilities, whereas React Testing Library focuses on testing the rendered output of components to ensure they work as intended.
2. How do I mock API calls in Jest when testing React components?
You can mock API calls in Jest by using its built-in jest.mock
function. This allows you to mock modules or functions, such as API calls, and simulate different responses during your tests. You can then assert that the component behaves as expected based on these mocked responses.
3. What is the best approach to test asynchronous code in React components?
Jest supports asynchronous testing using async/await syntax. To test asynchronous code in React components, you should use async
functions in your tests and await
on functions that return promises. You can also use helper methods like waitFor
from React Testing Library to wait for updates in the DOM after an asynchronous operation.
4. How do I simulate user events like clicks or input changes in React Testing Library?
React Testing Library provides the fireEvent
and userEvent
utilities to simulate user interactions such as clicks, typing, and form submissions. These tools let you mimic real user actions in your tests and verify how the component responds to them.
5. Why is it important to use screen
for querying elements in React Testing Library?
screen
is a utility provided by React Testing Library that allows you to access all elements in the rendered DOM. It simplifies queries by making them more readable and reducing the need to destructure multiple functions from render
. Using screen
ensures your tests are more intuitive and maintain