Glossary

Integration test

An integration test checks a real connection to selected infrastructure. It need not, and should not, call a production third-party service to be useful.

Short definition

Real connection of selected parts in a controlled environment.

An integration test can check an SQL query against a test database, persistence in Redis, or HTTP-response mapping in an adapter. The parts are real, but the environment is reserved for testing and its data has a controlled lifecycle.

It is not a unit test that isolates a rule using doubles. It is not an E2E browser test either, for which Playwright is often used. A contract test verifies an agreed contract between provider and client; an integration test can reveal more local detail but cannot guarantee compatibility with every external version.

What it solves

Errors a double cannot faithfully replace

The goal is not to run everything together, but to choose a technical boundary with meaningful risk.

  • entity mapping, constraints, and a transaction against a real test database
  • serialization, headers, and HTTP-adapter error states against a controlled test server
  • TTL, keys, and invalidation against a dedicated Redis instance
  • PHP application configuration and dependencies in a Docker container
  • fixture handling and cleanup without sharing data with another run

Practical example

A repository saves and loads an order from a test database

The test starts a real database adapter against a database used only for tests. It does not use production connection details and rolls back a transaction or cleans data after the scenario. This catches an SQL, schema, or mapping error an in-memory fake may miss.

Independence matters in parallel runs too. Every test needs its own data, transaction, database, or safely isolated namespace. Fixtures should create the smallest required initial state, not a copy of production personal data.

PHP

final class OrderRepositoryTest extends TestCase
{
    public function testItPersistsAndLoadsOrder(): void
    {
        $order = Order::create('order-123');
        $this->repository->save($order);

        $loadedOrder = $this->repository->get('order-123');

        self::assertSame('order-123', $loadedOrder->id());
    }
}

Scope diagram

From a rule to an adapter and user scenario

Diagram: unit test = isolated rule; integration test = real adapter and test service; E2E = whole interface scenario.

  1. Unit test Controls inputs with a stub, mock, or fake and quickly checks a rule without a network, database, or container.
  2. Integration fixture The test creates small data in a test database, Redis namespace, or local HTTP test server.
  3. Real boundary The application calls a specific driver or adapter. The test checks SQL, serialization, cache, configuration, or mapping.
  4. Cleanup and isolation After a test, data is rolled back, deleted, or a container is removed. Another test must not depend on its order.
  5. E2E and contract test E2E checks a user flow through an interface. A contract test keeps an agreed provider-client contract; neither automatically replaces integration.

Tools and boundaries

A test runner is not the test boundary.

PHPUnit can run unit and integration tests. The question is whether the test calls a real technology part.

PHPUnit

Provides runner, assertions, and fixture lifecycle. It does not automatically design a test database or cleanup for a project.

Docker

Can provide reproducible test services, but must not accidentally connect a test to a shared volume or production variables.

Playwright

A browser E2E tool. It can sit above integration layers but tests a broader user boundary.

Important concepts

Use real infrastructure, not uncontrolled production surroundings.

A good integration suite targets concrete boundaries and knows how to clean after itself.

Test database

It has its own URL, user, and data. A transaction can speed cleanup, but must reflect what the adapter really does.

Redis and queue

Use dedicated Redis or a namespace and clean keys after a test. Cache or queue state must not be shared with local development or production.

HTTP adapter

Test request, headers, timeout, and response mapping against a local test server, stub server, or recorded scenario. A production external API is not a required test dependency.

Container

Docker can provide a database or broker version close to production. Service image, ports, readiness, and cleanup are part of the test design.

Fixture and cleanup

A fixture creates required state; cleanup removes it after a test. A test that works only after another test is not isolated.

Benefits and limits

More confidence in the connection at the cost of time and infrastructure.

Benefits

  • reveals errors in SQL, configuration, serialization, or a real driver
  • checks migration, constraint, or cache behavior more realistically
  • gives confidence in an adapter without calling a production external service
  • can protect a critical integration boundary against regressions

Things to watch

  • tests are slower than unit tests and need infrastructure lifecycle
  • shared data, ports, or cache namespaces lead to flaky results
  • a production API is unsuitable for routine automation because of cost, data, and availability
  • an integration test alone does not check a whole user flow or the provider’s shared contract

When it makes sense

When risk lies at a boundary between application and technology.

An integration test belongs where a specific SQL query, ORM mapping, Redis client, HTTP adapter, or container configuration needs to actually work. Not every getter needs its own infrastructure; choose boundaries with a history of failure or high impact.

Leave most scenarios to fast unit tests. A smaller integration suite over controlled services fills the missing confidence. Use E2E tests for a few critical flows that truly need a browser and whole interface.

What to keep in mind

Test infrastructure is part of the contract being checked.

A slow or unreliable integration test is not inevitable; it often reveals unclear isolation or lifecycle.

  • separate test connection details and credentials from development and production
  • wait for service readiness instead of a fixed sleep
  • create small fixtures and then roll back, delete, or remove a namespace
  • do not run tests against a production external API; use a test server, sandbox, or contract scenario
  • name ports, containers, and keys so parallel runs do not collide
  • schedule integration tests in CI/CD by risk and give a readable failure output

Frequently asked questions

Integration testing without mix-ups

Must an integration test call a production external API?

No. A local test server, provider sandbox, or controlled contract scenario is normally better. A production API is unstable, may cost money, and can use sensitive data.

Is a PHPUnit test automatically a unit test?

No. PHPUnit is a runner. If a test uses a real database, Redis, or HTTP adapter, it is an integration test by scope.

Is an integration test an E2E test?

No. An integration test targets a selected technical boundary. An E2E test drives the whole application from a user perspective, often through a browser.

Is a contract test the same thing?

No. A contract test verifies agreed format and expectations between systems. An integration test verifies a concrete implementation collaboration in a test environment.

How I maintain development quality

I check technical boundaries with a real service, but in a controlled environment.

For PHP applications, I combine test databases, containers, and quality gates so integrations inspire confidence without risking operational data.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.