How to Test React Functional Components

Testing React functional components is a crucial step in delivering reliable, bug-free web applications. This guide covers the essential strategies and tools required to test functional components effectively, focusing on React Testing Library and Jest. You will learn how to render components, interact with elements, handle asynchronous actions, and write assertions that mimic real user behavior.

Choosing the Right Testing Tools

The industry standard for testing React functional components relies on two primary tools: Jest and React Testing Library (RTL).

Writing a Basic Component Test

To test a functional component, you render it using RTL’s render method, query the DOM to find specific elements, and use Jest’s expect function to assert that those elements behave as intended.

Here is a simple functional component:

// Greeting.jsx
import React from 'react';

export default function Greeting({ name }) {
  return <h1>Hello, {name || 'Guest'}!</h1>;
}

Here is the corresponding test:

// Greeting.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

describe('Greeting Component', () => {
  test('renders default guest message', () => {
    render(<Greeting />);
    const headingElement = screen.getByText(/hello, guest!/i);
    expect(headingElement).toBeInTheDocument();
  });

  test('renders user name when passed as a prop', () => {
    render(<Greeting name="Alice" />);
    const headingElement = screen.getByText(/hello, alice!/i);
    expect(headingElement).toBeInTheDocument();
  });
});

Simulating User Interactions

Most functional components respond to user input, such as clicks, typing, or form submissions. The @testing-library/user-event package is the recommended library for simulating these interactions because it dispatches realistic browser events.

Consider this Counter component:

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

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

The test below simulates a click event to verify the state update:

// Counter.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('increments count on button click', async () => {
  render(<Counter />);
  const user = userEvent.setup();
  
  const button = screen.getByRole('button', { name: /increment/i });
  const countText = screen.getByText(/count: 0/i);
  
  expect(countText).toBeInTheDocument();
  
  await user.click(button);
  
  expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});

Handling Asynchronous Operations

Functional components often fetch data from APIs or perform delayed state updates. To test asynchronous behavior, use RTL’s findBy queries or the waitFor utility. findBy queries return a promise that resolves when the queried element appears in the DOM, retrying for a default timeout of 1000ms.

// AsyncUser.jsx
import React, { useEffect, useState } from 'react';

export default function AsyncUser() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    setTimeout(() => {
      setUser({ name: 'John Doe' });
    }, 500);
  }, []);

  if (!user) return <div>Loading...</div>;
  return <div>User: {user.name}</div>;
}

Testing this asynchronous component requires awaiting the element to appear:

// AsyncUser.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import AsyncUser from './AsyncUser';

test('displays user name after loading', async () => {
  render(<AsyncUser />);
  
  expect(screen.getByText(/loading.../i)).toBeInTheDocument();
  
  const userText = await screen.findByText(/user: john doe/i);
  expect(userText).toBeInTheDocument();
});

Mocking API Calls and Hooks

To isolate functional components during integration tests, you should mock external network requests and custom hooks. This ensures tests are predictable, fast, and do not make actual network requests.

You can mock a global fetch request using Jest:

// UserProfile.jsx
import React, { useState, useEffect } from 'react';

export default function UserProfile() {
  const [email, setEmail] = useState('');

  useEffect(() => {
    fetch('/api/user')
      .then((res) => res.json())
      .then((data) => setEmail(data.email));
  }, []);

  return <div>{email ? `Email: ${email}` : 'Loading...'}</div>;
}
// UserProfile.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';

beforeEach(() => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ email: 'user@example.com' }),
    })
  );
});

afterEach(() => {
  jest.restoreAllMocks();
});

test('fetches and displays user email', async () => {
  render(<UserProfile />);
  const emailText = await screen.findByText(/email: user@example.com/i);
  expect(emailText).toBeInTheDocument();
  expect(global.fetch).toHaveBeenCalledTimes(1);
});