Testing React Functional Components with Jest & React Testing Library

Published on 29.7.2019

Testing is a critical part of the development process, ensuring your components work as expected and remain maintainable over time. React Testing Library (RTL) and Jest provide an excellent combination for testing functional components, especially those using hooks and local state.

In this post, we’ll explore how to test React functional components using Jest and React Testing Library. We’ll cover:

  • Testing components that use hooks (like useState and useEffect).
  • Mocking API calls in your tests.
  • Writing meaningful tests that focus on behavior rather than implementation details.

Let’s dive into it!


Setting Up the Environment

Before we start writing tests, ensure that you have the necessary libraries installed. If you created your React app with create-react-app, Jest and React Testing Library should already be set up. But if you need to install them manually, use:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest

React Testing Library provides a set of utilities to test components in a way that mirrors how they’re used in real applications. Jest, on the other hand, is a testing framework that helps us run and organize our tests.


Testing Stateful Functional Components

Let’s start by testing a simple functional component that uses React state. We’ll create a counter component that increments a number when a button is clicked.

Counter Component:

// src/Counter.js
import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
    </div>
  );
};

export default Counter;

This component maintains the count in state and increments it by 1 when the button is clicked. Now, let’s write a test for this component.

Testing Counter Component:

// src/Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('increments the counter when button is clicked', () => {
  render(<Counter />);

  // Verify initial count is 0
  const countElement = screen.getByText(/count:/i);
  expect(countElement).toHaveTextContent('Count: 0');

  // Find the increment button and click it
  const button = screen.getByText(/increment/i);
  fireEvent.click(button);

  // Verify count has been incremented
  expect(countElement).toHaveTextContent('Count: 1');
});

Explanation:

  1. render: The render function from RTL is used to render the component in a virtual DOM.
  2. screen.getByText: This function finds elements by their text content. Here, we use it to find the count and button elements.
  3. fireEvent.click: This simulates a click event on the increment button.
  4. Assertions: We use expect to assert that the count updates correctly when the button is clicked.

Testing Components with useEffect Hook

Let’s now write a test for a component that uses the useEffect hook to fetch data from an API.

FetchData Component:

// src/FetchData.js
import React, { useState, useEffect } from 'react';

const FetchData = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();
      setData(result);
    };

    fetchData();
  }, []); // Empty dependency array means it runs once, like componentDidMount

  return <div>{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'Loading...'}</div>;
};

export default FetchData;

This component fetches data from an API and displays it once the data is available. The key thing here is that we’re dealing with an asynchronous operation inside useEffect, so we need to account for that in our test.

Testing FetchData Component with Mocked API Call:

// src/FetchData.test.js
import { render, screen, waitFor } from '@testing-library/react';
import FetchData from './FetchData';

// Mocking the global fetch function
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ message: 'Hello, world!' }),
  })
);

test('fetches and displays data', async () => {
  render(<FetchData />);

  // Assert that loading text appears initially
  expect(screen.getByText(/loading.../i)).toBeInTheDocument();

  // Wait for the data to be fetched and rendered
  await waitFor(() => expect(screen.getByText(/"message": "Hello, world!"/i)).toBeInTheDocument());

  // Assert that the correct data is displayed
  expect(screen.getByText(/"message": "Hello, world!"/i)).toBeInTheDocument();
});

Explanation:

  1. Mocking API calls: We use Jest’s jest.fn() to mock the global fetch function. The mock returns a resolved promise with the expected data. This allows us to test the component without actually hitting an API.
  2. waitFor: Since fetching data is asynchronous, we use waitFor to wait for the data to be rendered before making assertions.
  3. Assertions: We check that the initial “Loading…” text is displayed and then confirm that the fetched data is rendered once the fetch is complete.

Mocking API Calls in Tests

Mocking API calls is a common scenario in tests, especially when you want to isolate your components from real API calls. In the above example, we mocked the fetch function using jest.fn().

You can also mock other parts of your app like localStorage or other third-party libraries. Here’s a quick example for mocking localStorage:

// Mocking localStorage in tests
beforeEach(() => {
  const mockSetItem = jest.fn();
  const mockGetItem = jest.fn().mockReturnValue('mocked data');
  Object.defineProperty(global, 'localStorage', {
    value: { setItem: mockSetItem, getItem: mockGetItem },
  });
});

test('accesses localStorage', () => {
  // Your test code here
  expect(localStorage.getItem('key')).toBe('mocked data');
});

Best Practices for Testing React Components

  1. Test behavior, not implementation: Tests should focus on how the component behaves rather than testing its implementation details. For example, test that clicking a button increments a count, not the exact internal mechanism.
  2. Use mocks and spies: Mock external API calls and services to isolate your components from dependencies and focus on testing their logic.
  3. Write meaningful assertions: Ensure your assertions reflect what the user will experience. Don’t just check that elements exist—check that they appear with the correct values after an action has been taken.

Conclusion

Testing React components using Jest and React Testing Library is an essential skill that ensures your components behave as expected and remain maintainable. By writing tests for functional components, including those that use hooks like useState and useEffect, you can catch potential bugs early and improve the reliability of your app.

  • Jest makes it easy to mock functions and APIs for testing.
  • React Testing Library focuses on testing components as a user would interact with them, encouraging better testing practices.
  • Mocking API calls allows for faster and more isolated tests, especially when dealing with asynchronous data.

Happy testing!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *