Tags: web-dev concept

Integration Testing

Date: 2026-08-17


Testing pieces working together rather than in isolation. It’s where most real bugs live, because most real bugs are at the seams — and it’s the tier that gives the best confidence per minute of test runtime.


An integration test exercises several components together with their real collaborators — a real database, a real render, a real HTTP handler — rather than mocked substitutes.

The seams are where things break. Individually correct units frequently don’t fit: a field named differently, a null nobody expected, a transaction boundary in the wrong place.

What it looks like

A component, rendered properly:

test('shows a validation error for a bad postcode', async () => {
  render(<CheckoutForm />)
 
  await user.type(
    screen.getByLabelText('Postcode'), 'XXX')
  await user.click(
    screen.getByRole('button', { name: 'Continue' }))
 
  expect(
    await screen.findByText(/postcode wasn't recognised/i)
  ).toBeVisible()
})

Note what’s being asserted: what a user sees, found the way a user would find it — by label and by role. No component internals, no state inspection — Unit Testing.

An API route, with a real database:

test('rejects an order for out-of-stock items', async () => {
  await db.products.insert({ id: 1, stock: 0 })
 
  const res = await request(app)
    .post('/orders')
    .send({ productId: 1, quantity: 1 })
 
  expect(res.status).toBe(409)
  expect(await db.orders.count()).toBe(0)
})

The second assertion matters as much as the first — it verifies nothing was written, which a status-code check alone doesn’t.

Query by role, not by test id

// resilient, and tests accessibility
screen.getByRole('button', { name: 'Add to basket' })
screen.getByLabelText('Postcode')
 
// fragile, tests nothing about usability
screen.getByTestId('add-btn')
container.querySelector('.btn-primary')

Querying by accessible role and name means the test fails if the element stops being reachable by assistive technology. It’s an accessibility check arriving free with every test you were writing anyway — Screen Readers, Accessible Forms.

Test IDs are a fallback, legitimate when there’s genuinely no accessible handle — and that absence is usually itself the bug.

Real dependencies versus fakes

DependencyApproach
Your databaseReal — a test instance or container
Your own modulesReal — that’s the point
Third-party HTTP APIsIntercept at the network layer
Payment providersTheir sandbox, or a fake
TimeFreeze it
RandomnessSeed it

Use a real database. An in-memory substitute has different behaviour around transactions, constraints, collation and concurrency — which is exactly the class of bug this tier exists to catch — Transactions and ACID.

Containers make this easy, and it’s their strongest use case in testing — Containers.

Intercept HTTP at the network layer

// intercepts the actual request
server.use(
  http.get('/api/prices', () =>
    HttpResponse.json({ price: 4999 }))
)

Better than mocking your fetch wrapper, because your real client code runs — including the URL construction, headers, error handling and parsing, which is where client bugs actually are.

Test isolation

The recurring failure at this tier, because real state persists between tests:

EACH TEST
  starts from a known state
  cleans up, or runs in a transaction
    that rolls back
  does not depend on order
  can run in parallel

Transaction rollback per test is the cleanest pattern where the framework supports it — fast, and complete.

A test that passes alone and fails in the suite is a state-leak, and it’s the most common source of flakiness here — Flaky Tests, Test Data.

The cost, honestly

UNIT           milliseconds
INTEGRATION    tens to hundreds of ms
E2E            seconds

Integration tests are 10–100× slower than unit tests, which is why the pyramid puts them in the middle. In practice the confidence per second is usually the best of the three tiers, which is the argument for weighting towards them anyway — Testing Strategy.

Keep the suite parallelisable and it stays viable well past the point people expect.