Stop 12 · TestingTopics · 2/9

Integration Testing

Part of the Backend Developer Roadmap

In short

Every function can pass its unit tests and the app can still break, because the pieces don't fit. Integration tests check that your route, service and real database actually work together.

Testing how multiple backend pieces work together — a route handler plus its service plus a real (or test) database — closer to real usage than isolated unit tests.

The blob character watches a fully assembled pressure cooker whistle on the stove.

Think of it like this

The assembled pressure cooker

The lid, gasket and whistle can each be perfect, and the cooker still leaks if the gasket doesn't sit right. You only find out by putting it together and cooking. Integration tests assemble the real parts, route plus database, and check the whistle blows.

Watch it happen

One test, real parts

Test
Express app
Test DB

TRUNCATE orders

1/5Before each test, the orders table is wiped so the test starts clean.

The big ideas

What you need to know

Real parts, wired together

Use the real router, services and a real database. Fake only things you don't own, like payment gateways.

A separate test database

Point tests at their own database, often in Docker, never at your development or production data.

Reset between tests

Truncate or roll back before each test, so one test's leftovers never break another.

Slower, so fewer

Integration tests take longer than unit tests. Cover the important paths, not every branch.

See it in code

Walk through the code

ts
1import { beforeAll, beforeEach, afterAll, test, expect } from "vitest";2import { app } from "../src/app";3import { pool } from "../src/db";4 5let server: ReturnType<typeof app.listen>;6beforeAll(() => new Promise<void>((done) => { server = app.listen(0, () => done()); }));7beforeEach(() => pool.query("TRUNCATE orders RESTART IDENTITY"));8afterAll(async () => { server.close(); await pool.end(); });9 10test("POST /orders saves the order", async () => {11  const url = `http://localhost:${(server.address() as { port: number }).port}/orders`;12  const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ item: "chai", qty: 2 }) });13  expect(res.status).toBe(201);14  const { rows } = await pool.query("SELECT item, qty FROM orders");15  expect(rows).toEqual([{ item: "chai", qty: 2 }]);16});

1/4Start the real app on a random free port before the tests run.

Point DATABASE_URL at a throwaway test database, for example a Postgres container started just for the tests.

Save yourself the pain

Common mistakes

Oops

Running integration tests against your shared development database.

Do this instead

Tests will wipe or depend on real data. Use a dedicated test database that can be reset freely.

Oops

Letting tests depend on running in a certain order.

Do this instead

Reset state before each test. Every test should pass when run on its own.

Your mission

Test a route end to end

~40 min

Quick check

Unit tests pass, but POST /orders fails in production because of a wrong column name in the SQL. Which test would have caught it?

Go deeper

  1. 1Write a test that hits a real API endpoint backed by a test database.
  2. 2Set up and tear down test data cleanly between test runs.
  3. 3Notice the trade-off: integration tests give more confidence but run slower than unit tests.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge