Testing React Applications

Write reliable tests for React components using React Testing Library, Jest, and Vitest. Learn unit testing, integration testing, and best practices.

Testing ensures your React components work correctly and helps prevent regressions. Modern React testing focuses on testing behavior, not implementation.

Testing Philosophy

Test components the way users interact with them. Focus on what the component does, not how it's built internally.

React Testing Library

The standard library for testing React components. It provides utilities to render components and query the DOM like a user would.

Testing User Interactions

Simulate clicks, typing, and other events with userEvent. Verify the component responds correctly.

Async Testing

Handle asynchronous operations with waitFor and findBy queries. Test loading states, data fetching, and error handling.

Mocking

Mock API calls, modules, and timers to isolate components and control test conditions.

Code Examples

Basic Component Test

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

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);

    expect(screen.getByRole('button', { name: /click me/i }))
      .toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const handleClick = vi.fn();
    const user = userEvent.setup();

    render(<Button onClick={handleClick}>Submit</Button>);

    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when loading', () => {
    render(<Button loading>Submit</Button>);

    expect(screen.getByRole('button')).toBeDisabled();
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });
});

Testing Async Behavior

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

// Mock the API
vi.mock('./api', () => ({
  fetchUser: vi.fn(),
}));

import { fetchUser } from './api';

describe('UserProfile', () => {
  it('shows loading state initially', () => {
    fetchUser.mockImplementation(() => new Promise(() => {}));

    render(<UserProfile userId="123" />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('displays user data when loaded', async () => {
    fetchUser.mockResolvedValue({
      name: 'John Doe',
      email: 'john@example.com',
    });

    render(<UserProfile userId="123" />);

    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });

    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });

  it('shows error message on failure', async () => {
    fetchUser.mockRejectedValue(new Error('Failed to fetch'));

    render(<UserProfile userId="123" />);

    expect(await screen.findByText(/error/i)).toBeInTheDocument();
  });
});

Testing Forms

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

describe('LoginForm', () => {
  it('submits with email and password', async () => {
    const handleSubmit = vi.fn();
    const user = userEvent.setup();

    render(<LoginForm onSubmit={handleSubmit} />);

    // Fill out the form
    await user.type(
      screen.getByLabelText(/email/i),
      'test@example.com'
    );
    await user.type(
      screen.getByLabelText(/password/i),
      'password123'
    );

    // Submit
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });

  it('shows validation errors', async () => {
    const user = userEvent.setup();

    render(<LoginForm onSubmit={vi.fn()} />);

    // Submit without filling form
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(screen.getByText(/email is required/i)).toBeInTheDocument();
    expect(screen.getByText(/password is required/i)).toBeInTheDocument();
  });
});

Frequently Asked Questions

Should I test implementation details like state?

No. Test the component's behavior from a user's perspective. If a button click should show a message, test for the message, not the state change. This makes tests more resilient to refactoring.

How do I test components that use context?

Wrap the component in the necessary providers during testing. Create a custom render function that includes all required providers to keep tests clean and consistent.

What's the difference between queryBy, getBy, and findBy?

getBy throws if not found (use when element should exist). queryBy returns null if not found (use to verify absence). findBy is async and waits for the element (use for async content).

Need React Help?

Slashdev.io builds production-ready React applications for businesses of all sizes.

Get in Touch