What Is Integration Testing and How Do You Test Real Dependencies?

Updated

Integration testing verifies that separate modules, services, and external dependencies work together correctly — not just that each piece works alone. You need it whenever your code talks to a database, queue, cache, or third-party API, because the bugs that matter most often live in those connections rather than inside a single function. This article covers how integration testing differs from unit and API testing, the main ways to run it (real dependencies, mocks, and record-and-replay), where it fits in CI/CD, and what to check when it breaks.

What integration testing actually verifies

A unit test isolates one function or class and stubs everything around it. An integration test does the opposite: it exercises the boundary where two or more components meet and checks that data and control flow survive the trip.

That boundary can be:

  • Internal — module A calls module B inside the same application.
  • Service-to-service — your service calls another service over HTTP or gRPC.
  • Service-to-dependency — your code reads/writes a database, publishes to a queue, or hits an external API.

The failure modes integration tests catch are the ones unit tests structurally cannot: a schema mismatch, a serialization bug, a wrong SQL column, a queue message that never gets acknowledged, or a contract change in a dependency you don't control.

Integration vs. unit vs. API testing

These layers answer different questions, and you generally want all three.

Layer What it tests Dependencies Typical speed
Unit testing One function/class in isolation Stubbed or mocked Milliseconds
Integration testing Interactions between modules, services, and real dependencies Real, mocked, or recorded Seconds to minutes
API testing Request/response behavior of an endpoint Often mocked or a test server Seconds
E2E testing A full user flow through the whole system Everything real Minutes

The overlap is real: API testing is a kind of integration testing when it exercises a real backend and its dependencies. Tools like Postman are commonly used to design and manually test API requests, while integration testing is more concerned with whether the assembled system behaves correctly end to end. As Keploy's FAQ puts it, a platform can support both — generating API tests from traffic and specifications, while its record-and-replay approach creates integration and regression tests with virtualized databases, queues, APIs, and other dependencies.

Three ways to handle dependencies

The core design decision in integration testing is what you do about the database, queue, and external services your code depends on.

1. Real dependencies

Spin up an actual Postgres, Redis, or Kafka instance (often in Docker) and point your tests at it. Highest fidelity, but slow, flaky, and expensive to keep in sync across environments.

2. Mocks and stubs

Replace dependencies with hand-written fakes. Fast and deterministic, but the mock is only as good as your understanding of the real dependency — and it drifts the moment the real thing changes. Tools such as WireMock usually require developers to define and maintain these mocks by hand.

3. Record-and-replay of real traffic

Capture actual requests, responses, and dependency interactions, then replay them against a new build. Keploy does this by capturing incoming requests and outgoing dependency calls using eBPF and network-level interception, without requiring application code changes. It converts the captured behavior into editable tests and mocks, then replays them to catch regressions. This sits between the other two: higher fidelity than hand-written mocks, cheaper than standing up every real dependency.

Where integration tests fit in CI/CD

Integration tests earn their keep when they run automatically before merge. The pattern:

  1. Record behavior from a real environment (local, staging, or production) — requests, responses, and dependency calls.
  2. Store the resulting tests and mocks as versioned artifacts alongside your code.
  3. Replay them in CI against each new build.
  4. Fail the pipeline on response, schema, contract, or performance regressions before the code is merged or deployed.

Keploy supports running tests locally or in CI/CD pipelines, and its enterprise capabilities extend recording to Kubernetes, staging, and production environments. The core local record-and-replay platform is open source under the Apache 2.0 license.

This matters more as AI-generated code becomes common: a coding agent can produce a change that compiles and passes unit tests while silently breaking a dependency contract. Replaying recorded behavior against that change is what surfaces the break.

What to check when integration tests break

Most integration failures trace back to the environment, not the code. When a test that passed yesterday fails today, check these in order:

  • Environment drift — different database version, missing migration, or a config value that differs between local and CI.
  • Stale mocks — the real dependency changed its response shape but your mock still returns the old one (or vice versa).
  • Dependency version changes — a library upgrade altered serialization, timeouts, or retry behavior.
  • Contract and schema mismatches — a field was renamed, made nullable, or removed on one side of the boundary.
  • Test data state — leftover rows, unacknowledged queue messages, or a cache that wasn't cleared between runs.
  • Timing and ordering — async operations that the test assumes complete before it asserts.

A useful habit: when a mock and a real dependency disagree, treat the real dependency as the source of truth and regenerate the mock from recorded traffic rather than editing it by hand.

Choosing an approach

There's no single right answer — pick based on what you're protecting against:

  • Unit tests for logic inside a single component.
  • Hand-written mocks when the dependency is simple, stable, and you need speed.
  • Real dependencies when fidelity is critical and you can afford the infrastructure.
  • Record-and-replay when you want realistic dependency behavior without maintaining mocks by hand, and you want regression coverage that grows from actual usage.

The practical goal is the same across all of them: catch the interaction bugs that unit tests can't see, and catch them before they reach production.