Integration Testing
Integration tests verify that multiple components work together — an API handler plus its database queries, a service plus an external dependency, or several modules wired into a working slice of the system. They sit in the middle of the testing pyramid: higher confidence than unit tests (they catch wiring, serialization, and config bugs units miss) but cheaper and faster than full end-to-end tests.
The key insight: unit tests validate logic, integration tests validate wiring. Most real-world bugs live in the seams — the query that doesn't match the schema, the middleware that drops a header, the migration that breaks an assumption — and that's exactly what integration tests cover.
TL;DR
- Unit tests validate logic; integration tests validate wiring.
- Keep integration tests deterministic and isolated.
- Run a real database where possible, with clean state per test.
- Use contract tests to prevent breaking API changes.
Quick Example
Test the API as a black box — real server, real request, assert the response:
Integration vs Unit vs E2E
See Unit Testing and Playwright.
What to Integrate
The highest-value targets are the seams where bugs hide:
- API handlers + DB queries
- Authorization rules + data access
- Migrations + query assumptions
- Background jobs + persistence
Database Integration Testing
- Use a dedicated test database — never your dev or prod database.
- Reset state between tests with transactions (rollback after each) or truncation.
- Seed minimal fixtures — just what the test needs.
⚠️ Avoid coupling tests to a large, shared fixture dataset — it makes tests fragile and interdependent. Each test should set up the minimal data it relies on.
API Integration Tests
Test your API as a black box: start the server, make HTTP requests, and assert status codes and response shape. This catches routing, middleware, auth, and serialization issues that unit tests can't see. See REST API Design and API Security.
Contract Testing
Contract tests ensure a provider doesn't break its consumers and that schemas stay compatible. They're especially important in microservices, where a service can deploy independently and silently break a downstream consumer. The contract is the agreed request/response shape; both sides test against it.
Best Practices
- Control time — inject a fake clock for time-dependent logic.
- Don't call real third-party services — stub them at the boundary.
- Use stable IDs and deterministic data — no reliance on random or wall-clock values.
- Clean state per test — transactions or truncation, never leftover rows.
- Generous-but-bounded timeouts — long enough to avoid flakiness, short enough to fail fast.
Common Mistakes
Order-dependent, shared-state tests
Using integration tests to do E2E's job
FAQ
Should integration tests use a real database or a mock?
Prefer a real database (the same engine as production, in a test instance or container). Mocking the database for integration tests defeats the purpose — you're specifically trying to validate that your queries, migrations, and ORM mappings actually work against real SQL. Reset state between tests with transactions or truncation to keep them isolated and fast.
How do I keep integration tests from being flaky?
Eliminate non-determinism: don't call real third-party services (stub them), control the clock, use fixed IDs and seeded data rather than random values, and reset database state per test. Flakiness almost always traces back to shared state, real network calls, timing assumptions, or test-ordering dependencies — remove those and tests stabilize.
What's the right ratio of unit to integration to E2E tests?
The classic testing pyramid: many fast unit tests at the base, a moderate layer of integration tests for the wiring, and a thin layer of E2E tests for critical user journeys. Inverting it (lots of slow E2E tests, few unit tests) produces slow, flaky suites. Integration tests are the valuable middle — use them for seams, not for full user flows.
What is contract testing and when do I need it?
Contract testing verifies that the interface between two services stays compatible — the provider's responses still satisfy what consumers expect. You need it when independently-deployed services talk to each other (microservices), because a provider can change a response shape and break consumers without any shared test catching it. Tools like Pact formalize and automate the contract.
Related Topics
- Unit Testing — The layer below
- Test-Driven Development (TDD) — Driving design with tests
- API Testing — Testing API contracts
- Playwright — E2E for full flows
- Microservices — Where contracts matter most