A Guide to Testing React Components with Jest and React Testing Library

A Guide to Testing React Components with Jest and 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

Edit Abbhiishek/jest-testing/main

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 under src 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

A simple interface for a to-do list application with a text field labeled "Add a new todo," a blue "Add" button, and three filter options: "All," "Active," and "Completed."

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:

  1. It imports the necessary testing utilities from react testing and the Counter component.

  2. Jest uses a describe block to group related jest tests for the Counter component.

  3. The first jest test case checks if the initial count is rendered as 0.

  4. The second testcase clicks the increment button and checks if the count increases to 1.

  5. The third test clicks the decrement button and checks if the count decreases to -1.

  6. 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

Did you find this article valuable?

Support Keploy Community Blog by becoming a sponsor. Any amount is appreciated!