An integration test is an automated test that verifies how two or more components behave together, for example application code with a real database, a message broker, a file system or another service. In software testing, it sits between the unit test, which isolates one small piece of code, and the end-to-end test, which exercises the whole system through its user interface or public entry points.
Integration tests catch the defects that live at the boundaries: wrong SQL, mismatched data formats, incorrect configuration, serialization errors and misunderstood API contracts. These failures are invisible to unit tests that replace every collaborator with a mock.
How an integration test works
An integration test follows the familiar arrange, act, assert structure, but the arrange step prepares real infrastructure instead of fakes. It starts or connects to a database, loads a known data set, and configures the components under test as they would be configured in production.
The act step calls the integrated code through a meaningful entry point, such as a repository method, a service function or an HTTP endpoint of the application. The assert step then checks observable results across the boundary: rows written to the database, messages published to a queue, or the exact JSON returned by the endpoint. Finally, a teardown step resets state so that tests remain independent.
The following table lists the typical boundaries that integration tests cover and what each detects.
| Boundary | What is exercised | Defects typically found |
|---|---|---|
| Database | Queries, migrations, transactions | Invalid SQL, missing indexes, constraint violations |
| Message broker | Publishing and consuming events | Serialization errors, wrong topic names |
| HTTP API | Routing, validation, serialization | Wrong status codes, broken JSON contracts |
| External service | Client code against a real or simulated service | Authentication and timeout handling |
| File system or object storage | Reading and writing files | Path, encoding and permission problems |
Integration tests in the test pyramid
The test pyramid recommends many fast unit tests, fewer integration tests and only a small number of end-to-end tests. Integration tests occupy the middle layer because they are slower and more complex than unit tests but far cheaper and more stable than full end-to-end runs.
| Aspect | Unit test | Integration test | End-to-end test |
|---|---|---|---|
| Scope | One function or class | Several components and real infrastructure | Whole system through its interface |
| Typical duration | Milliseconds | Tens of milliseconds to seconds | Seconds to minutes |
| Dependencies | Replaced by mocks or stubs | Real database, broker or service | Fully deployed environment |
| Failure diagnosis | Precise | Moderate | Often difficult |
| Flakiness risk | Very low | Moderate | High |
Some teams prefer a testing trophy or honeycomb shape that puts integration tests at the center, especially for services whose logic is mostly data access and API wiring. Such code has little isolated logic to unit-test, so integration tests give the best confidence per test.
Approaches and scopes of integration testing
The term covers several scopes, and teams should agree on which one they mean. A narrow integration test checks a single boundary, such as one repository class against a real database. A broad integration test wires many components together and resembles a smaller end-to-end test.
Classic integration strategies describe the order in which modules are combined, which still matters in large systems and embedded software.
| Strategy | How it proceeds | Suitable when |
|---|---|---|
| Big bang | Combine all modules at once | Very small systems only |
| Top-down | Start from high-level modules, stub lower ones | Early validation of main workflows |
| Bottom-up | Start from low-level modules, drive them with test harnesses | Infrastructure and libraries mature first |
| Sandwich or hybrid | Combine top-down and bottom-up | Large layered systems |
| Contract testing | Verify each side against a shared contract | Microservices owned by different teams |
Building reliable integration tests
The biggest practical challenge is providing realistic dependencies cheaply. Modern practice favors disposable containers started for the test run, for example a real PostgreSQL or Kafka instance launched through a container library, instead of a shared test server that accumulates stale data.
- Use the same engine as production: an in-memory database with a different SQL dialect hides real defects.
- Isolate test data: wrap each test in a rolled-back transaction or truncate tables between tests.
- Control time and randomness: inject clocks and seeds so results are deterministic.
- Simulate third parties at the network level: an HTTP stub server tests real client code without calling a paid external API.
- Run in parallel safely: give each worker its own schema or container.
Integration tests also contribute to test coverage, but coverage numbers alone say little about boundary quality. A useful target is that every query, every external call and every published event type is exercised by at least one integration test.
Common problems and their causes
Poorly designed integration suites become slow and flaky, and developers start ignoring their failures. The table below links the most frequent symptoms to causes and remedies.
| Symptom | Likely cause | Remedy |
|---|---|---|
| Intermittent failures | Shared state or timing assumptions | Isolate data, wait on conditions instead of fixed sleeps |
| Suite takes too long | Starting infrastructure per test | Start containers once per suite, reset data per test |
| Passes locally, fails in CI | Environment differences | Containerized dependencies with pinned versions |
| Bugs reach production anyway | Too many mocks inside integration tests | Use real components at the tested boundary |
| Hard to understand failures | Tests cover too much at once | Narrow the scope to one boundary per test |
Understanding it simply
Testing each part of a car separately, the engine on a bench and the brakes on a rig, proves that each part works alone. It does not prove that the parts work once they are bolted together.
An integration test is the check after assembly. It connects real pieces of a program, such as the code and its database, and confirms that they understand each other. Many expensive bugs hide exactly at these connection points.
Applied in practice
A team builds an order service that saves orders in PostgreSQL and publishes an event to a message broker. Its unit tests all pass because the database and broker are mocked. In production, however, orders with long customer notes fail, because a database column is limited to 255 characters and the mock never enforced that limit.
The team adds integration tests that start real PostgreSQL and broker containers at the beginning of the CI run. One test saves an order with a 1,000-character note and checks both the stored row and the published event. The test reproduces the failure, a migration widens the column, and similar boundary defects are now caught before release. The suite of 60 integration tests adds about three minutes to the pipeline, a cost the team accepts.