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.
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.
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 on top of DOM Testing Library
, which is a very lightweight 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 involves querying the DOM for nodes in a way that's similar to how the user finds elements on the page.
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 the Assertion library. Mocking allows jest to create fake versions of functions or modules for testing.
In this section, we’ll set up a simple React app with create-react-app
for our testing environment using Jest and React Testing library. You can choose vite
any other react framework.
Feel free to clone the sandbox below and follow along
npx create-react-app jest-testing-blog
cd jest-testing-blog
CRA comes preconfigured with a jest testing environment, for other frameworks, we might need to install it manually.
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
Jest will look for test files with any of the following popular naming conventions:
.js
suffix in __tests__
folders..test.js
suffix..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
To demonstrate the Jest and React Testing Library, we would be using a simple Todo application.
Features of the Todo app:
This project is small enough to be manageable but includes enough functionality to showcase various testing scenarios.
The app will include the following components:
Jest provides a simple structure for writing test cases.
describe('Group of tests', () => {
test('individual test', () => {
// Test code
});
});
describe
: Groups related tests together. Useful when we are doing integration testing which consists 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 supports asynchronous code
test('async test', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
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:
describe
block to group-related jest tests for the Counter component.
We can usually run the test cases using jest with:
npm test
which test all our test cases 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 that we know how jest and its syntax work, let's move into writing test cases for our simple todo application.
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:
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', () => {
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', () => {
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', () => {
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:
we can also have multiple test files for each separate component inside TodoList
.
Create a AddTodos.test.js
under src/components/__test__
for testing the addtodo component
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');
});
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('');
});
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
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.
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;
Hurray 🎉! Our tests are running fine.
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.
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.
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.
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.
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.
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