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.
- Unit test Controls inputs with a stub, mock, or fake and quickly checks a rule without a network, database, or container.
- Integration fixture The test creates small data in a test database, Redis namespace, or local HTTP test server.
- Real boundary The application calls a specific driver or adapter. The test checks SQL, serialization, cache, configuration, or mapping.
- Cleanup and isolation After a test, data is rolled back, deleted, or a container is removed. Another test must not depend on its order.
- 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.