Integration Testing
Integration testing checks how multiple pieces work together. If unit tests verify individual ingredients, integration tests verify that the whole recipe tastes good.
You test real interactions between components, APIs, and state management. This catches problems that unit tests miss.
Testing Component Interactions
When components talk to each other, you need to verify the conversation flows correctly. A form sends data, a list displays it, a button triggers an action.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import TodoApp from './TodoApp'
test('adding a todo updates the list', async () => {
const user = userEvent.setup()
render(<TodoApp />)
const input = screen.getByPlaceholderText('Add a todo')
const button = screen.getByText('Add')
await user.type(input, 'Buy groceries')
await user.click(button)
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
expect(input).toHaveValue('')
})
test('completing a todo strikes it through', async () => {
const user = userEvent.setup()
render(<TodoApp />)
await user.type(screen.getByPlaceholderText('Add a todo'), 'Clean house')
await user.click(screen.getByText('Add'))
await user.click(screen.getByRole('checkbox'))
expect(screen.getByText('Clean house')).toHaveClass('completed')
})
These tests simulate real user actions. Type in the field, click the button, check the result. If the interaction breaks, the test catches it immediately.
User Events
The userEvent library simulates realistic user interactions. It handles delays, focus events, and keyboard behavior that simple click events miss.
import userEvent from '@testing-library/user-event'
test('form validates before submitting', async () => {
const user = userEvent.setup()
render(<LoginForm />)
await user.click(screen.getByText('Submit'))
expect(screen.getByText('Email is required')).toBeInTheDocument()
expect(screen.getByText('Password is required')).toBeInTheDocument()
await user.type(screen.getByLabelText('Email'), 'invalid-email')
await user.click(screen.getByText('Submit'))
expect(screen.getByText('Invalid email format')).toBeInTheDocument()
await user.clear(screen.getByLabelText('Email'))
await user.type(screen.getByLabelText('Email'), 'valid@test.com')
await user.type(screen.getByLabelText('Password'), 'secret123')
await user.click(screen.getByText('Submit'))
expect(screen.queryByText('Email is required')).not.toBeInTheDocument()
})
This test walks through the entire validation flow. Empty fields, invalid email, valid submission. Each step mirrors real user behavior.
API Mocking
Integration tests often need to simulate server responses. MSW (Mock Service Worker) intercepts network requests and returns controlled data.
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { render, screen, waitFor } from '@testing-library/react'
import ProductList from './ProductList'
const server = setupServer(
http.get('/api/products', () => {
return HttpResponse.json([
{ id: 1, name: 'Widget', price: 9.99 },
{ id: 2, name: 'Gadget', price: 19.99 }
])
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('displays product list from API', async () => {
render(<ProductList />)
await waitFor(() => {
expect(screen.getByText('Widget')).toBeInTheDocument()
expect(screen.getByText('Gadget')).toBeInTheDocument()
})
})
MSW works at the network level, so your actual fetch code runs unmodified. The test verifies the full flow from API call to rendered output.
Try it Yourself →