Integration and System Testing

4 min read

Once unit tests confirm individual functions work, the next questions are: do they work together? and does the whole application behave correctly? Those are integration testing and system testing — two layers of the test pyramid that catch the kinds of bugs unit tests cannot.

The pyramid in motion

The test pyramid stacks four layers, from cheapest at the bottom to most expensive at the top:

  1. Unit tests — one function, one class. Run in milliseconds.
  2. Integration tests — multiple components, including real dependencies (a service plus its database).
  3. System tests — the whole application as a black box.
  4. End-to-end tests — system tests driven through the user interface.

Each layer catches things the layer below cannot, but each is also slower, more brittle, and harder to maintain. The pyramid shape — many at the bottom, few at the top — is the cost-efficient distribution.

What integration testing actually covers

Integration tests verify that components correctly speak to each other. The components might be:

  • A service and its database, exercised through a real connection.
  • Two services talking over HTTP or gRPC.
  • A component and the third-party API it depends on.
  • A queue producer and a queue consumer.

Crucially, integration tests use real dependencies where possible — a real Postgres in a container, a real HTTP call to a test instance, a real queue. Mocks and stubs are used sparingly, because the bugs you are looking for are exactly the bugs that mocks hide: schema mismatches, serialisation issues, network behaviour, transaction semantics.

A typical integration test might:

  • Start a fresh database container.
  • Apply migrations.
  • Insert a known test user.
  • Call the service's "update profile" endpoint.
  • Query the database directly to confirm the row changed correctly.
  • Call the "get profile" endpoint to confirm the response shape.

The test passes only if all of those steps succeed.

What system testing actually covers

System testing treats the whole application as a black box and exercises it end-to-end. There are no mocks, no stubs, no test doubles — every component is the real thing, deployed in something close to production conditions. The goal is to verify that the system as a whole delivers the user-visible behaviour.

System tests answer questions integration tests cannot:

  • Does logging in actually create a session that the cart service can read?
  • Does a checkout API call eventually result in a confirmation email being sent?
  • Does cancelling an order release the reserved inventory after the right delay?
  • Does the audit log record every state change correctly?

These tests are usually driven through public APIs or the UI. UI-driven system tests use frameworks like Cypress, Playwright, or Selenium to simulate a real user clicking through real flows.

Why both layers matter

It is tempting to think you can skip integration testing if you have good unit tests and a few system tests. In practice that gap is exactly where the most expensive bugs live. Two services can each be unit-tested to perfection but still fail to talk to each other because:

  • One was deployed with an old schema.
  • They disagree on what "null" means in a JSON field.
  • The auth token format changed and one side did not update.
  • A timeout is too short on one side, too long on the other.

System tests would catch some of these but slowly and at high cost. Integration tests catch them in seconds, in CI, on the pull request that introduced them.

Where each layer breaks down

  • Integration tests become slow if they spin up too much. Keep them surgical — one service plus its real dependencies, not the whole platform.
  • System tests become flaky if the environment is unstable. Invest in deterministic test data and reliable environments, or your suite will lose the team's trust.
  • Both can become a dumping ground for tests that should be unit tests. If a bug can be reproduced in a single function, the test for it belongs at the unit level — not the system level.

What you should walk away with

Integration and system tests verify what unit tests cannot — the behaviour that emerges when components meet reality. They are slower and pricier than unit tests, but they catch the specific bugs that hide in the seams. Up next: UAT, alpha, and beta testing — the human-driven layers that sit on top of all of this.

// tip to track lessons you complete and pick up where you left off across devices.