Skip to content

Integration Testing

An integration test verifies that two or more units work together — your code plus a real, or realistically faked, collaborator. Where a unit test replaces everything around the subject, an integration test lets the seams be real: a database, a message queue, an HTTP layer, another module.

Integration tests catch the bugs unit tests structurally cannot: mismatched contracts, wrong SQL, misconfigured serialization, wrong transaction boundaries.

Use it for

  • The boundary between your code and a datastore, queue, cache or external HTTP API
  • Wiring: dependency injection, configuration, migrations
  • Multi-step flows inside one deployable (e.g. controller → service → repository)

Avoid it when

  • The concern is pure logic → a fast unit test is better
  • The whole system across the network must be exercised → that is end-to-end
  • The dependency is another team’s service you cannot run → use contract testing
Tool Ecosystem Notes
Testcontainers Multi (Java, Go, Node, Python, .NET) Real DB/queue/broker in Docker, disposed after the test
Supertest Node.js Drive an HTTP server in-process
Spring Boot Test Java @SpringBootTest, slices, embedded DB support
WireMock Multi Stub HTTP dependencies
MockServer Multi Expect/verify HTTP interactions
pytest + testcontainers Python Same containers, Python fixtures
httptest Go In-process HTTP servers for tests
Docker Compose Any Bring up a dependency graph for the suite
Terminal window
npm install -D vitest testcontainers supertest
import { PostgreSqlContainer } from '@testcontainers/postgresql';
const pg = await new PostgreSqlContainer().start();
const url = pg.getConnectionUri(); // real PostgreSQL, per run
import request from 'supertest';
import { app } from '../src/app';
it('creates and reads back a user', async () => {
const created = await request(app)
.post('/users')
.send({ name: 'Ada' })
.expect(201);
const res = await request(app)
.get(`/users/${created.body.id}`)
.expect(200);
expect(res.body.name).toBe('Ada');
});
  • Fake the boundaries you cannot own. Use Testcontainers or a real dev DB; mock only genuinely external, uncontrollable services.
  • Isolate per test. Roll back transactions or recreate schema per test so runs are independent and parallelizable.
  • Seed with builders. Factory functions beat a shared fixtures.sql that drifts.
  • Do not assert on internals of a dependency. Assert on your behaviour as observed through the boundary.
  • Keep the suite fast enough to run often. Integration tests are slower than units; prune duplicates and run them in CI on every push.

Self-assessment quiz

Pick one answer per question, then press Check answers.

  1. What does an integration test add over a unit test?
  2. Which tool runs a real database in Docker, disposed after the test?
  3. Which node tool drives an HTTP server in-process for integration tests?
  4. A dependency is another team's service you cannot run locally. What is the better choice?
  5. Why must each integration test isolate its data?