Jest Testing Guide for React Apps: Components, Hooks, and Async

In modern frontend development, building scalable React applications requires robust automated testing to prevent regressions and verify component behavior across state transitions. While Jest provides the test runner, assertion library, and mocking engine, **React Testing Library (RTL)** encourages testing components from the user's perspective rather than focusing on internal implementation details.

This comprehensive guide explores how to test React components, user interactions, custom hooks, and asynchronous API calls using Jest and React Testing Library.

1. Testing React Components and User Interactions

When testing UI components, you render the component using RTL, query elements using semantic queries like `screen.getByRole`, and simulate user interactions using `userEvent`.

C
// Counter.jsx
import { 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>
    );
}

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

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

2. Handling Asynchronous Code and API Mocking

Components that fetch data asynchronously require `screen.findBy*` queries or `waitFor` assertions to handle loading and resolved states correctly.

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

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

    useEffect(() => {
        fetch(`https://api.example.com/users/${userId}`)
            .then((res) => res.json())
            .then((data) => setUser(data));
    }, [userId]);

    if (!user) return <p>Loading...</p>;
    return <h1>{user.name}</h1>;
}

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

beforeEach(() => {
    global.fetch = jest.fn(() =>
        Promise.resolve({
            json: () => Promise.resolve({ name: 'Jane Doe' }),
        })
    );
});

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

test('renders user profile after async fetch', async () => {
    render(<UserProfile userId={1} />);
    
    expect(screen.getByText(/loading.../i)).toBeInTheDocument();
    
    const heading = await screen.findByRole('heading', { name: /jane doe/i });
    expect(heading).toBeInTheDocument();
});

3. Testing Custom React Hooks

Because React hooks cannot be rendered outside of a component context, `@testing-library/react` provides `renderHook` to test custom hook state and side effects in isolation.

C
// useCounter.js
import { useState } from 'react';

export function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);
    const increment = () => setCount((c) => c + 1);
    return { count, increment };
}

// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('should initialize and increment custom hook', () => {
    const { result } = renderHook(() => useCounter(10));
    
    expect(result.current.count).toBe(10);
    
    act(() => {
        result.current.increment();
    });
    
    expect(result.current.count).toBe(11);
});

Conclusion

Mastering Jest and React Testing Library—from simulating user interactions and handling asynchronous API requests to testing custom hooks in isolation—empowers frontend engineers to deliver reliable, highly maintainable React applications.