Stop 13 · Testing the Full StackTopics · 3/6

Integration Testing

Part of the Full Stack 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 frontend and backend pieces work together — a form component plus the API call it triggers, or an API route plus a real test database.

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 backend integration test that hits a real endpoint backed by a test database.
  2. 2Write a frontend integration test that fills a form and asserts the resulting API call was made.
  3. 3Notice these give more confidence than unit tests, at the cost of running slower.

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