Share
1

React Testing Strategies: Unit, Component, and Integration Testing

by ObserverPoint · July 19, 2025

Writing tests for your React applications is an indispensable practice for ensuring code quality, preventing regressions, and facilitating confident refactoring. A well-tested application leads to fewer bugs, better maintainability, and a smoother development workflow. This article will guide you through essential testing strategies in React, focusing on Unit Testing with Jest, Component Testing with React Testing Library, techniques for Mocking API calls, and key Integration Testing principles.

By embracing these testing methodologies, you can build more resilient and reliable React applications.


1. Unit Testing with Jest

Unit testing focuses on testing individual, isolated units or functions of your code in isolation from the rest of your application. The goal is to verify that each unit works as expected, given specific inputs. For JavaScript and React projects, Jest is the most popular and recommended testing framework, known for its simplicity, speed, and powerful features.

Key Concepts in Jest:

  • `describe()`: Groups related tests together.
  • `test()` or `it()`: Defines an individual test case.
  • `expect()`: The assertion function you use with “matchers” to test values.
  • Matchers: Functions that let you assert something about a value (e.g., `toBe`, `toEqual`, `toContain`, `not`).

Example (Simple Utility Function Unit Test):

<!-- src/utils/math.ts -->
export const add = (a: number, b: number): number => a + b;
export const subtract = (a: number, b: number): number => a - b;

<!-- src/utils/math.test.ts -->
import { add, subtract } from './math';

describe('Math Utilities', () => {
    test('add function should correctly add two numbers', () => {
        expect(add(1, 2)).toBe(3);
        expect(add(-1, 1)).toBe(0);
        expect(add(0, 0)).toBe(0);
    });

    test('subtract function should correctly subtract two numbers', () => {
        expect(subtract(5, 3)).toBe(2);
        expect(subtract(10, 0)).toBe(10);
        expect(subtract(0, 5)).toBe(-5);
    });
});
    

Jest comes pre-configured with most React development setups (like Create React App or Vite), making it straightforward to start writing unit tests.


2. Component Testing with React Testing Library

While Jest is the runner, React Testing Library (RTL) is the preferred library for testing React components. Unlike Enzyme (an older alternative), RTL focuses on testing components the way users interact with them and ensures your tests resemble actual user behavior rather than internal implementation details.

This approach promotes accessible and robust components, as you’re testing the user-facing output.

Key Principles of React Testing Library:

  • Querying Elements: Prioritize queries that a user would use to find elements (e.g., `getByRole`, `getByLabelText`, `getByText`, `getByPlaceholderText`). Avoid querying by `className` or `id` unless necessary for accessibility (`aria-label`, `htmlFor`).
  • User Events: Simulate user interactions using `@testing-library/user-event` (e.g., `click`, `type`, `tab`).
  • Assertions: Use Jest’s matchers along with `@testing-library/jest-dom` for DOM-specific assertions (e.g., `toBeInTheDocument`, `toBeDisabled`).

Example (Simple Button Component Test):

<!-- src/components/MyButton.tsx -->
import React from 'react';

interface MyButtonProps {
    onClick: () => void;
    children: React.ReactNode;
    disabled?: boolean;
}

const MyButton: React.FC<MyButtonProps> = ({ onClick, children, disabled = false }) => {
    return (
        <button onClick={onClick} disabled={disabled}>
            {children}
        </button>
    );
};

export default MyButton;

<!-- src/components/MyButton.test.tsx -->
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; // For extended matchers like toBeInTheDocument
import MyButton from './MyButton';

describe('MyButton Component', () => {
    test('renders with correct text', () => {
        render(<MyButton onClick={() => {}}>Click Me</MyButton>);
        // Query by role (button) and then by accessible name (text content)
        expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
    });

    test('calls onClick handler when clicked', () => {
        const handleClick = jest.fn(); // Create a mock function
        render(<MyButton onClick={handleClick}>Test Button</MyButton>);

        fireEvent.click(screen.getByText(/test button/i)); // Simulate a click
        expect(handleClick).toHaveBeenCalledTimes(1); // Assert mock function was called once
    });

    test('is disabled when disabled prop is true', () => {
        render(<MyButton onClick={() => {}} disabled>Disabled Button</MyButton>);
        expect(screen.getByRole('button', { name: /disabled button/i })).toBeDisabled();
    });
});
    

3. Mocking API Calls

When testing components that interact with external services (like REST APIs), you don’t want your tests to make actual network requests. This would make tests slow, unreliable (dependent on external service availability), and difficult to run offline. Mocking API calls involves replacing real network requests with simulated responses.

Jest has powerful built-in mocking capabilities, and libraries like MSW (Mock Service Worker) offer a more advanced, network-level mocking solution.

Methods for Mocking API Calls:

  1. Jest `mock` functions: For simple cases, you can mock global `fetch` or `axios` methods directly.
  2. MSW (Mock Service Worker): Recommended for robust mocking. It intercepts network requests at the service worker level (in browsers) or Node.js level (in tests), allowing you to define handlers for specific routes and return mock data. This is closer to how real network requests behave.

Example (Mocking `fetch` with Jest):

<!-- src/services/userService.ts -->
export interface User {
    id: number;
    name: string;
    email: string;
}

export const fetchUser = async (id: number): Promise<User> => {
    const response = await fetch(`https://api.example.com/users/${id}`);
    if (!response.ok) {
        throw new Error('Failed to fetch user');
    }
    return response.json();
};

<!-- src/components/UserProfile.tsx -->
import React, { useState, useEffect } from 'react';
import { fetchUser, User } from '../services/userService';

interface UserProfileProps {
    userId: number;
}

const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
    const [user, setUser] = useState<User | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);

    useEffect(() => {
        const getUser = async () => {
            try {
                setLoading(true);
                const fetchedUser = await fetchUser(userId);
                setUser(fetchedUser);
            } catch (err) {
                setError((err as Error).message);
            } finally {
                setLoading(false);
            }
        };
        getUser();
    }, [userId]);

    if (loading) return <div>Loading user profile...</div>;
    if (error) return <div style={{ color: 'red' }}>Error: {error}</div>;
    if (!user) return <div>No user data.</div>;

    return (
        <div>
            <h2>User Profile</h2>
            <p><b>Name:</b> {user.name}</p>
            <p><b>Email:</b> {user.email}</p>
        </div>
    );
};

export default UserProfile;

<!-- src/components/UserProfile.test.tsx -->
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import UserProfile from './UserProfile';

// highlight-start
// Mock the global fetch function
global.fetch = jest.fn(() =>
    Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ id: 1, name: 'John Doe', email: 'john.doe@example.com' }),
    } as Response) // Type assertion for Response
);

describe('UserProfile Component', () => {
    // Reset mocks before each test to ensure isolation
    beforeEach(() => {
        (fetch as jest.Mock).mockClear();
    });

    test('displays user profile after fetching data', async () => {
        render(<UserProfile userId={1} />);

        // Initially displays loading state
        expect(screen.getByText(/loading user profile.../i)).toBeInTheDocument();

        // Wait for the asynchronous fetch operation to complete and content to appear
        await waitFor(() => {
            expect(screen.getByRole('heading', { name: /user profile/i })).toBeInTheDocument();
            expect(screen.getByText(/name: john doe/i)).toBeInTheDocument();
            expect(screen.getByText(/email: john.doe@example.com/i)).toBeInTheDocument();
        });

        // Ensure fetch was called with the correct URL
        expect(fetch).toHaveBeenCalledTimes(1);
        expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
    });

    test('displays error message if fetching fails', async () => {
        // highlight-next-line
        (fetch as jest.Mock).mockImplementationOnce(() =>
            Promise.resolve({
                ok: false, // Simulate a network error or bad response
                status: 404,
                json: () => Promise.resolve({ message: 'User not found' }),
            } as Response)
        );

        render(<UserProfile userId={999} />);

        await waitFor(() => {
            expect(screen.getByText(/error: failed to fetch user/i)).toBeInTheDocument();
        });
    });
});
// highlight-end
    

For more complex mocking scenarios, especially across multiple tests and components, consider setting up MSW, which provides a declarative way to mock API endpoints globally.


4. Integration Testing Principles

Integration testing verifies that different modules or services of your application work together correctly as a group. In the context of React, this often means testing how multiple components interact, how data flows through them, and how they behave when connected to a larger application context (e.g., Redux store, React Router).

Integration tests are typically built upon the same tools as component tests (Jest and React Testing Library) but focus on broader scenarios rather than isolated units.

Key Principles of Integration Testing:

  • Test User Flows: Focus on end-to-end user journeys or critical features that involve multiple components. For example, a user logging in, navigating to a page, filling out a form, and submitting it.
  • Render Groups of Components: Instead of rendering a single component, render a parent component that composes several children, or even render an entire slice of your application (e.g., a specific route).
  • Mock External Dependencies: Just like with component tests, mock API calls and other external services to ensure tests are fast and reliable.
  • Test the “Seams”: Pay attention to the points where different parts of your application connect. For example, how a component dispatches an action to a Redux store, or how a navigation link changes the URL.
  • Balance Granularity: Integration tests are more expensive to write and maintain than unit tests. Don’t test every single permutation; focus on critical paths and common interactions. A good strategy is the “testing pyramid”: many unit tests, fewer integration tests, and very few end-to-end tests.

Example (Simple Integration Scenario – Counter with Display):

<!-- src/components/Counter.tsx -->
import React, { useState } from 'react';

const Counter: React.FC = () => {
    const [count, setCount] = useState(0);

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

export default Counter;

<!-- src/App.tsx (Simplified App to integrate Counter) -->
import React from 'react';
import Counter from './components/Counter';
import UserProfile from './components/UserProfile'; // Assuming UserProfile from above

const App: React.FC = () => {
    return (
        <div>
            <h1>My React App (Integration Example)</h1>
            <Counter />
            <hr />
            <UserProfile userId={1} />
        </div>
    );
};

export default App;

<!-- src/App.test.tsx (Integration Test) -->
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import App from './App'; // Test the entire App component or a slice of it

// Re-use the fetch mock from UserProfile.test.tsx for consistency in integration tests
global.fetch = jest.fn(() =>
    Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ id: 1, name: 'Jane Doe', email: 'jane.doe@example.com' }),
    } as Response)
);

describe('App Integration Tests', () => {
    beforeEach(() => {
        (fetch as jest.Mock).mockClear();
    });

    test('renders both Counter and UserProfile components and their interactions', async () => {
        render(<App />);

        // Verify Counter functionality (integration between Counter and its display)
        expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
        fireEvent.click(screen.getByRole('button', { name: /increment/i }));
        expect(screen.getByText(/count: 1/i)).toBeInTheDocument();

        // Verify UserProfile functionality (integration with API mock)
        expect(screen.getByText(/loading user profile.../i)).toBeInTheDocument();
        await waitFor(() => {
            expect(screen.getByText(/name: jane doe/i)).toBeInTheDocument();
            expect(screen.getByText(/email: jane.doe@example.com/i)).toBeInTheDocument();
        });

        expect(fetch).toHaveBeenCalledTimes(1);
        expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
    });
});
    

Integration testing helps ensure that different parts of your application play nicely together, providing confidence that complex user flows work as intended.

By combining unit tests for isolated logic, component tests for user-centric UI behavior, and integration tests for multi-component interactions and data flows, you can establish a robust testing strategy for your React applications.


References

You may also like