Skip to content

Integration Testing

Integration tests verify interactions between components: repositories with databases, buses with handlers, middleware chains.

Scope

  • Repository implementations with real databases
  • Command/Query buses with handlers
  • Middleware behavior
  • Event dispatching and listening

Test Structure

test('SessionMiddleware starts session', function (): void {
    $session = new FakeSession();
    expect($session->isStarted())->toBeFalse();

    $middleware = new SessionMiddleware($session);
    $request = new ServerRequest('GET', '/');
    $handler = new FakeRequestHandler(new Response(200));

    $middleware->process($request, $handler);

    expect($session->isStarted())->toBeTrue();
});

Repository Integration Tests

test('SqliteBookRepository persists and retrieves book', function (): void {
    $pdo = new PDO('sqlite::memory:');
    $pdo->exec(file_get_contents('database/schema.sqlite.sql'));

    $repository = new SqliteBookRepository($pdo);
    $book = Book::create($id, $title, $author, $isbn);

    $repository->save($book);
    $retrieved = $repository->find($book->id());

    expect($retrieved)->not->toBeNull()
        ->and($retrieved->id())->toBe($book->id());
});

Test Database Setup

beforeEach(function (): void {
    $this->pdo = new PDO('sqlite::memory:');
    $this->pdo->exec(file_get_contents('database/schema.sqlite.sql'));
});

Multi-Database Repository Testing

Repository tests run against all database implementations (InMemory, SQLite, MySQL, PostgreSQL) using shared test templates.

Base Test Template Pattern

Define tests once, register for each implementation:

// BaseBookRepositoryTests.php
function registerBaseBookRepositoryTests(): void
{
    test('saves and retrieves a book', function (): void {
        $book = Book::create($id, $title, $author, $isbn);
        $this->repository->save($book);

        $found = $this->repository->getById($id);

        expect($found)->not->toBeNull()
            ->and($found->getId())->toBe($id);
    });

    test('findAll returns all books', function (): void {
        // Test implementation...
    });

    // Additional shared tests...
}

Implementation-Specific Test Files

Each repository implementation includes the base tests and configures its database:

// SqliteBookRepositoryTest.php
require_once __DIR__.'/BaseBookRepositoryTests.php';
registerBaseBookRepositoryTests();

beforeAll(function () use (&$dbName): void {
    $dbName = PdoRegistry::initializeSqlite(true);
});

beforeEach(function (): void {
    PdoRegistry::getConnection()->beginTransaction();
    $driver = new SqliteDriver(PdoRegistry::getConnection());
    $this->repository = new SqliteBookRepository($driver, new TimestampIdGenerator());
});

afterEach(function (): void {
    PdoRegistry::getConnection()->rollBack();
});

PdoRegistry

Centralized database connection management for tests:

// Initialize database with production schema
PdoRegistry::initializeSqlite(schemaProduction: true);
PdoRegistry::initializeMysql(schemaProduction: true);
PdoRegistry::initializePostgresql(schemaProduction: true);

// Get shared connection
$pdo = PdoRegistry::getConnection();

// Cleanup after tests
PdoRegistry::cleanupMysql($dbName);
PdoRegistry::cleanupPostgresql($dbName);

MySQL and PostgreSQL create unique database names per test run to allow parallel execution.

Test Isolation with Transactions

Each test runs in a transaction that rolls back after completion:

beforeEach(function (): void {
    PdoRegistry::getConnection()->beginTransaction();
});

afterEach(function (): void {
    PdoRegistry::getConnection()->rollBack();
});

This ensures tests don't affect each other while avoiding slow database recreation.

Using Fake Objects

The tests/*/Fake/ directories contain test doubles for sessions, handlers, and other services. See Test Doubles for details.

Running Tests

task tests:integration
task tests:integration -- tests/Phexium/Integration/Session/

Best Practices

  • Isolate each test (fresh database state)
  • Prefer InMemory implementations for speed
  • Don't mock what you're testing
  • Keep tests focused on component boundaries
  • Use transactions for cleanup when possible
  • Share test logic via base test functions

See Also