Integration Testing
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints note 'automated: no' and the code_pattern is 'No integration test suite; only unit tests; business logic untested against real DB schema.' PHPUnit/Pest can run integration tests if they exist, but the absence of integration tests — or their misconfiguration (shared state, real external services) — is only visible through deliberate code review or when bugs surface in staging/production. No tool automatically flags the gap.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix points to running integration tests against a real DB in a Docker container in CI with transaction rollback — this isn't a one-liner. It requires setting up Docker infrastructure, configuring CI pipelines, isolating test data, and potentially refactoring existing tests that share state or hit real external services. This spans multiple files and configuration layers.
Closest to 'persistent productivity tax' (b5). Integration testing applies to all PHP contexts (web, cli, queue-worker) per applies_to. Once adopted, every new feature or database change must be covered; misconfigured integration tests (shared state, slow suites) slow many work streams. However, a well-maintained suite is beneficial rather than purely a burden, so it doesn't reach b7.
Closest to 'serious trap' (t7). The misconception field states developers treat integration tests as 'just slow unit tests' to be minimised, missing that they catch contract mismatches between layers that unit tests with mocks entirely miss. The common_mistakes reinforce this: not running them in CI, sharing database state, and hitting real external services are all contradictions of what developers familiar only with unit testing would assume. This contradicts the mental model carried over from unit testing practices.
Also Known As
TL;DR
Explanation
Integration tests verify that components interact correctly when combined — e.g., that a repository correctly persists and retrieves domain objects from a real database, or that an HTTP controller returns the expected response for a given request. They are slower and more expensive than unit tests but catch bugs that only emerge from component interactions. In PHP, integration tests typically use a test database, in-memory SQLite, or containers (via Docker or Laravel TestContainers). Follow the Test Pyramid: many unit tests, fewer integration tests, few E2E tests.
Diagram
flowchart TD
subgraph Unit Tests - Isolated
UT[Test business logic<br/>all dependencies mocked<br/>fast milliseconds]
end
subgraph Integration Tests - Real Dependencies
IT[Test component interactions<br/>real DB real cache real queue<br/>slower seconds]
end
subgraph What Integration Tests Catch
N1[N+1 query bugs<br/>mocks hide these]
N2[Transaction isolation issues]
N3[ORM mapping problems]
N4[Migration correctness]
end
IT --> N1 & N2 & N3 & N4
style UT fill:#238636,color:#fff
style IT fill:#d29922,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Using real external services (payment APIs, email providers) in integration tests — use sandbox environments or contract tests.
- Not isolating test data — tests that share database state interfere with each other and produce random failures.
- Making integration tests so broad they take minutes to run — they should be slower than unit tests but not by orders of magnitude.
- Not running integration tests in CI — they catch the bugs that matter most.
Code Examples
// Mock that doesn't match real behaviour:
$mockRepo = $this->createMock(UserRepository::class);
$mockRepo->method('findByEmail')->willReturn($user); // Always returns a user
// Real DB: findByEmail throws on DB connection failure
// Integration test catches what the mock hides
class OrderApiTest extends TestCase {
use RefreshDatabase; // reset DB per test
public function test_place_order_stores_and_emails(): void {
Mail::fake();
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/orders', ['sku' => 'WIDGET-1', 'qty' => 2]);
$response->assertStatus(201);
$this->assertDatabaseHas('orders', ['user_id' => $user->id]);
Mail::assertSent(OrderConfirmation::class);
}
}