Integration Testing Guidelines

Integration tests form the middle layer of our testing pyramid. Their purpose is to verify that different “units” of our application work together correctly.

What is an Integration Test?

While a unit test might test a single React component in isolation, an integration test would test a group of components working together on a page, or a component that fetches data from a real (but controlled) API endpoint.

Key Characteristic: Integration tests are less mocked than unit tests. They cross boundaries between different parts of the system.

Scenarios for Integration Testing

  1. Component Composition:

    • Testing a form component that is composed of several smaller input components. The test would verify that the form as a whole manages its state correctly and that the submit button works as expected.
  2. Frontend to Backend Communication:

    • Testing a page that fetches data from an API and displays it.
    • For this, we often use Mock Service Worker (MSW) to intercept network requests and return mock responses. This allows us to test the full data flow (component renders fetches data displays data) without needing a live backend.
  3. Authentication Flows:

    • Testing that a user can log in and is then redirected to their dashboard. This involves the login form, the authentication service, and the routing system.

Tools

  • Jest and React Testing Library: We use the same tools as for unit testing, but the tests are structured to cover more ground.
  • Mock Service Worker (MSW): For mocking API responses at the network level.

Best Practices

Focus on a Feature or User Flow

  • An integration test should focus on a specific feature (e.g., “user profile editing”) or a small user flow (e.g., “adding an item to the cart”).
  • Structure the test around the user’s actions and expected outcomes.

Example: Testing a Data-Fetching Component with MSW

Let’s say we have a component that fetches and displays a user’s name.

1. Set up MSW Handlers: Create a handler that describes which API endpoint to mock and what data to return.

// src/mocks/handlers.js
import { rest } from 'msw';
 
export const handlers = [
  rest.get('/api/user', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        name: 'John Doe',
      })
    );
  }),
];

2. Write the Integration Test: The test renders the component and waits for the mock data to be displayed.

// src/components/UserInfo.test.tsx
import { render, screen } from '@testing-library/react';
import UserInfo from './UserInfo';
 
test('fetches and displays the user name', async () => {
  // Arrange
  render(<UserInfo />);
 
  // Act: The component will automatically fetch data on render.
 
  // Assert
  // We use findBy* because the data is loaded asynchronously.
  const userName = await screen.findByText(/John Doe/i);
  expect(userName).toBeInTheDocument();
});

When to Write Integration Tests vs. Unit Tests

  • Use a unit test to check the logic of a single, isolated function or component (e.g., “does the formatDate function work correctly?”).
  • Use an integration test to check that different parts of your system are wired together correctly (e.g., “when the user clicks ‘Save’, is a POST request sent to the correct API endpoint?”).

A good balance of unit and integration tests provides high confidence that your application is working correctly, without the slowness and brittleness of relying only on end-to-end tests.