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

describe('Session lifecycle', function (): void {
    it('starts session on request', 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

describe('Persistence', function (): void {
    it('saves and retrieves a book', function (): void {
        $book = Book::create($id, $title, $author, $isbn);

        $this->repository->save($book);
        $retrieved = $this->repository->getById($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('app/demo/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

Tests are defined once in a base file using file-scope describe blocks, then included by each implementation:

// BaseBookRepositoryTests.php
describe('Persistence', function (): void {
    it('saves and retrieves a book', function (): void {
        $book = BookMother::create();
        $this->repository->save($book);

        $found = $this->repository->getById($book->getId());

        expect($found)->not->toBeNull()
            ->and($found->getId())->toEqual($book->getId());
    });
});

describe('Querying', function (): void {
    it('returns all books via findAll', function (): void {
        // Test implementation...
    });
});

Implementation-Specific Test Files

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

// SqliteBookRepositoryTest.php
pest()->group('integration');

require_once __DIR__.'/BaseBookRepositoryTests.php';

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                       # All integration tests
task tests:integration:phexium               # Phexium only
task tests:integration:appdemo               # AppDemo only

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 files with file-scope describe blocks

See Also