
Chapter Outline
Writing unit tests with Jest and React Testing Library
Testing is an integral part of modern web development. Unit testing ensures that individual components and functions work as expected, providing confidence in code stability and maintainability. In this article we’ll focus on writing unit tests using Jest and React Testing Library (RTL).
Jest is a popular JavaScript testing framework, while React Testing Library complements it by providing utilities to test React components in a way that mimics user interactions. Together, they form a powerful combination for writing robust unit tests in React.
Setting Up Jest and React Testing Library
Before we dive into writing tests, let’s set up a React project with Jest and React Testing Library.
Installation
If you're using a project created with create-react-app, Jest and React Testing Library come pre-installed. However, if you’re starting from scratch or using a custom setup, you can install the necessary packages:
bashnpm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event
Additionally, for TypeScript projects, install the type definitions:
bashnpm install --save-dev @types/jest @types/react @types/react-dom
Configuring Jest
If your project doesn’t already have a Jest configuration, you can initialize it with:
bashnpx jest --init
You’ll also need to set up a jest.config.js file if additional configuration is required, for example:
javascriptmodule.exports = {testEnvironment: "jsdom", // Required for React Testing LibrarysetupFilesAfterEnv: ["<rootDir>/jest.setup.js"], // Custom setup file};
Writing Unit Tests with Jest and React Testing Library
Let’s explore how to write tests for React components and functions.
Example 1: Testing a Simple Component
Component: Button
jsx1import React from "react";23function Button({ label, onClick }) {4 return <button onClick={onClick}>{label}</button>;5}67export default Button;
Test for Button Component
The test ensures that:
- The button renders correctly.
- It handles user interactions.
jsx1import React from "react";2import { render, screen, fireEvent } from "@testing-library/react";3import Button from "./Button";45test("renders Button and handles click", () => {6 const handleClick = jest.fn(); // Mock function for the click handler7 render(<Button label="Click Me" onClick={handleClick} />);89 // Assert that the button is rendered with the correct label10 const button = screen.getByRole("button", { name: /click me/i });11 expect(button).toBeInTheDocument();1213 // Simulate a click and assert that the handler is called14 fireEvent.click(button);15 expect(handleClick).toHaveBeenCalledTimes(1);16});
Example 2: Testing a Form with User Interaction
Component: LoginForm
jsx1import React, { useState } from "react";23function LoginForm({ onSubmit }) {4 const [email, setEmail] = useState("");5 const [password, setPassword] = useState("");67 const handleSubmit = (e) => {8 e.preventDefault();9 onSubmit({ email, password });10 };1112 return (13 <form onSubmit={handleSubmit}>14 <label>15 Email:16 <input17 type="email"18 value={email}19 onChange={(e) => setEmail(e.target.value)}20 />21 </label>22 <label>23 Password:24 <input25 type="password"26 value={password}27 onChange={(e) => setPassword(e.target.value)}28 />29 </label>30 <button type="submit">Login</button>31 </form>32 );33}3435export default LoginForm;
Test for LoginForm
jsx1import React from "react";2import { render, screen, fireEvent } from "@testing-library/react";3import LoginForm from "./LoginForm";45test("submits login form with correct data", () => {6 const handleSubmit = jest.fn(); // Mock function for the form submit handler7 render(<LoginForm onSubmit={handleSubmit} />);89 // Fill in the form fields10 fireEvent.change(screen.getByLabelText(/email/i), {11 target: { value: "test@example.com" },12 });13 fireEvent.change(screen.getByLabelText(/password/i), {14 target: { value: "password123" },15 });1617 // Submit the form18 fireEvent.click(screen.getByRole("button", { name: /login/i }));1920 // Assert that the handler is called with the correct data21 expect(handleSubmit).toHaveBeenCalledWith({22 email: "test@example.com",23 password: "password123",24 });25 expect(handleSubmit).toHaveBeenCalledTimes(1);26});
Example 3: Testing Asynchronous Code
Component: FetchUser
jsx1import React, { useEffect, useState } from "react";23function FetchUser({ userId }) {4 const [user, setUser] = useState(null);56 useEffect(() => {7 async function fetchUserData() {8 const response = await fetch(9 `https://jsonplaceholder.typicode.com/users/${userId}`10 );11 const data = await response.json();12 setUser(data);13 }14 fetchUserData();15 }, [userId]);1617 if (!user) return <p>Loading...</p>;18 return <div>{user.name}</div>;19}2021export default FetchUser;
Test for FetchUser
jsx1import React from "react";2import { render, screen, waitFor } from "@testing-library/react";3import FetchUser from "./FetchUser";45// Mock the fetch API6global.fetch = jest.fn(() =>7 Promise.resolve({8 json: () =>9 Promise.resolve({10 name: "John Doe",11 }),12 })13);1415test("fetches and displays user data", async () => {16 render(<FetchUser userId={1} />);1718 // Assert that the loading state is displayed19 expect(screen.getByText(/loading/i)).toBeInTheDocument();2021 // Wait for the user data to be rendered22 await waitFor(() =>23 expect(screen.getByText(/john doe/i)).toBeInTheDocument()24 );2526 // Assert that the fetch API was called with the correct URL27 expect(fetch).toHaveBeenCalledWith(28 "https://jsonplaceholder.typicode.com/users/1"29 );30});
Testing components within Vite
Jest is not fully supported by vite due to how the plugin system from vite works. However, a suitable alternative to Jest is Vitest. It is a Vite native testing framework.
To add Vitest to your project, run the following:
bashnpm install -D vitest vitest-browser-react @vitest/browser playwright
You may need to download the latest Chromium browser to run Playright:
bashnpx playwright install
In order to execute the test, add the following section to your package.json:
json{"scripts": {"test": "vitest"}}
Write your test for a component:
jsx1import { expect, test, vi } from 'vitest'2import { render } from 'vitest-browser-react'3import Button from './Button';45test('renders Button and handles click', () => {6 const handleClick = vi.fn()78 const { getByText, getByRole } = render(<Button label="Click Me" onClick={handleClick} />)910 // Assert that the button is rendered with the correct label11 const button = getByRole('button', { name: /click me/i })12 await expect.element(button).toBeInTheDocument()1314 // Simulate a click and assert that the handler is called15 await button.click()16 expect(handleClick).toHaveBeenCalledTimes(1);17});
Execute the test:
bashnpm run test
Common Jest Features
Jest Expect
javascript// Basic expectationsexpect(value).not.toBe(value).toEqual(value).toBeTruthy()// Booleanexpect(value).toBeFalse().toBeNull().toBeTruthy().toBeUndefined().toBeDefined()// Objectexpect(value).toContain(item).toContainEqual(item).toHaveLength(number).toBeInstanceOf(Class).toMatchObject(object).toHaveProperty(keyPath, value)// Numbersexpect(value).toBeCloseTo(number,numDigits).toBeGreaterThan(number).toBeGreaterThanOrEqual(number).toBeLessThan(number).toBeLessThanOrEqual(number)// Stringsexpect(value).toMatch(regexpOrString)// Errorsexpect(value).toThrow(error).toThrowErrorMatchingSnapshot()
Jest Mock functions
javascript// Mock functionsconst fn = jest.fn()const fn = jest.fn(n => n * n)// Callsconst fn = jest.fn()fn(123)fn(456)fn.mock.calls.length // → 2fn.mock.calls[0][0] // → 123fn.mock.calls[1][0] // → 456// Return valuesconst fn = jest.fn(() => 'hello')jest.fn().mockReturnValue('hello')jest.fn().mockReturnValueOnce('hello')// Instancesconst Fn = jest.fn()a = new Fn()b = new Fn()Fn.mock.instances// → [a, b]// Mock implementsconst fn = jest.fn().mockImplementationOnce(() => 1).mockImplementationOnce(() => 2)fn() // → 1fn() // → 2// Assertionsexpect(fn).toHaveBeenCalled().toHaveBeenCalledTimes(number).toHaveBeenCalledWith(arg1, arg2, ...).toHaveBeenLastCalledWith(arg1, arg2, ...).toHaveBeenCalledWith(expect.anything()).toHaveBeenCalledWith(expect.any(constructor)).toHaveBeenCalledWith(expect.arrayContaining([ values ])).toHaveBeenCalledWith(expect.objectContaining({ props })).toHaveBeenCalledWith(expect.stringContaining(string)).toHaveBeenCalledWith(expect.stringMatching(regexp))
Common Vitest Features
Vitest Expect
javascript// Basic expectationsexpect(value).not.toBe(value).toEqual(value).toBeTruthy()// Booleanexpect(value).toBeFalse().toBeNull().toBeTruthy().toBeFalsy().toBeUndefined().toBeDefined()// Objectexpect(value).toContain(item).toContainEqual(item).toHaveLength(number).toBeInstanceOf(Class).toMatchObject(object).toHaveProperty(keyPath, value)// Numbersexpect(value).toBeCloseTo(number,numDigits).toBeGreaterThan(number).toBeGreaterThanOrEqual(number).toBeLessThan(number).toBeLessThanOrEqual(number)// Stringsexpect(value).toMatch(regexpOrString)// Errorsexpect(value).toThrow(error).toThrowErrorMatchingSnapshot()// Arraysexpect(['Alice', 'Bob', 'Eve']).toHaveLength(number).toContain('Alice')// Exceptionexpect(fn).toThrowError(error)
Vitest Mock functions
javascript// Mock functionsconst fn = vi.fn()const fn = vi.fn(n => n * n)// Callsconst fn = vi.fn()fn(123)fn(456)fn.mock.calls.length // → 2fn.mock.calls[0][0] // → 123fn.mock.calls[1][0] // → 456// Return valuesconst fn = vi.fn(() => 'hello')vi.fn().mockReturnValue('hello')vi.fn().mockReturnValueOnce('hello')// Assertionsexpect(fn).toHaveBeenCalled().toHaveBeenCalledTimes(number).toHaveBeenCalledWith(arg1, arg2, ...).toHaveBeenLastCalledWith(arg1, arg2, ...).toHaveBeenCalledWith(expect.anything()).toHaveBeenCalledWith(expect.any(constructor)).toHaveBeenCalledWith(expect.arrayContaining([ values ])).toHaveBeenCalledWith(expect.objectContaining({ props })).toHaveBeenCalledWith(expect.stringContaining(string)).toHaveBeenCalledWith(expect.stringMatching(regexp))
Best Practices for Testing React Applications
- Test User Behavior, Not Implementation Details:
- Use React Testing Library’s utilities to mimic real user interactions.
- Avoid testing internal implementation, focusing instead on the rendered output and behavior.
- Mock External Dependencies:
- Use Jest’s mocking features to simulate API calls, timers, and other dependencies.
- Measure Test Coverage:
- Use Jest’s built-in coverage tool to identify untested parts of your code.
- Organize Tests:
- Place test files alongside their corresponding components using the
.test.tsxor.spec.tsxnaming convention.
- Place test files alongside their corresponding components using the