Home/Learn/React/Integration Tests — A Whole Feature

Integration Tests — A Whole Feature

Advanced
Testing

Rendering a real route with real providers and walking through a flow catches the wiring bugs that unit tests structurally cannot, at a fraction of the cost of an E2E suite.

Overview

The most valuable React tests are usually neither unit nor end-to-end. Rendering a route with the router, the query client and the auth provider in place, then completing a flow the way a user would, exercises the integration points where things actually break: a form that validates but never submits, a mutation that succeeds without refreshing the list, a guard that redirects the wrong way. These run in milliseconds, need no browser, and give most of the confidence of an E2E test — which is why the middle of the testing pyramid is where the effort belongs.

A Whole Flow

One test covering form, validation, mutation, invalidation and navigation.

One flow, four classes of bug
it('creates a problem and shows it in the list', async () => {
  const { user } = renderWithProviders(<App />, { route: '/admin/problems/new' })

  await user.type(screen.getByLabelText(/title/i), 'Two Sum')
  await user.selectOptions(screen.getByLabelText(/difficulty/i), 'easy')
  await user.click(screen.getByRole('button', { name: /publish/i }))

  // navigated
  expect(await screen.findByRole('heading', { name: 'Two Sum' })).toBeInTheDocument()

  // and the list was invalidated, not left stale
  await user.click(screen.getByRole('link', { name: /all problems/i }))
  expect(await screen.findByText('Two Sum')).toBeInTheDocument()
})

// This single test would have caught: a broken submit handler, a
// validation rule blocking valid input, a missing invalidation, and
// a wrong redirect. Four unit tests would have caught none of them,
// because each of those bugs lives BETWEEN the units.

Testing Routing and Guards

MemoryRouter gives you a real router with a controllable starting point.

MemoryRouter with a starting route
it('redirects a signed-out user to sign in', async () => {
  server.use(http.get('/api/me', () => new HttpResponse(null, { status: 401 })))

  renderWithProviders(<App />, { route: '/dashboard' })

  expect(await screen.findByRole('heading', { name: /sign in/i })).toBeInTheDocument()
})

it('returns the user to where they were going after signing in', async () => {
  const { user } = renderWithProviders(<App />, { route: '/dashboard' })
  await signIn(user)
  expect(await screen.findByRole('heading', { name: /dashboard/i })).toBeInTheDocument()
})

it('renders a 404 for an unknown route', () => {
  renderWithProviders(<App />, { route: '/nope' })
  expect(screen.getByText(/page not found/i)).toBeInTheDocument()
})

// Guards, redirects and 404s are pure wiring, invisible to unit
// tests, and among the most common things to break in a refactor.

Keeping the Suite Trustworthy

Where integration tests sit, and what makes them rot.

Factories, role queries, one behaviour per test
//        /\        E2E (Playwright) — a handful of critical journeys
//       /  \       INTEGRATION — feature flows, most of the value
//      /____\      Unit — pure logic and branchy functions

// What makes an integration suite rot:
//   - shared mutable fixtures, so tests depend on order
//   - assertions on incidental text that copy changes break
//   - one enormous test asserting fifteen things

// Instead:
//   - build fixtures per test with a factory
const makeProblem = (over = {}) => ({ id: 1, slug: 'two-sum', ...over })
//   - assert by role and accessible name, which survives copy edits
//   - one behaviour per test, named as a sentence about the user

// Run them in CI on every PR. An integration suite nobody runs is
// a suite that will be deleted the first time it goes red.

// And keep them fast: no arbitrary waits, no real network, no
// unnecessary providers.

Key Points to Remember

  • 1Integration tests render a real route with real providers and catch bugs that live between units
  • 2One flow test can cover validation, submission, cache invalidation and navigation at once
  • 3MemoryRouter with an initial route makes guards, redirects and 404s testable without a browser
  • 4Build fixtures per test with factories and query by role, so tests do not depend on order or exact copy
  • 5Integration tests are the widest useful layer — faster than E2E and far more revealing than unit tests alone

Interview Questions

Sign in to ask Aria
1

What kind of bug does an integration test catch that unit tests cannot?

Medium
2

How would you test that signing in returns the user to the page they originally requested?

Hard
3

What makes an integration test suite become unreliable over time?

Hard

Ask Aria about Integration Tests — A Whole Feature

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…