Skip to content

Testing Your Application

Muhammet Şafak edited this page May 24, 2026 · 1 revision

Testing Your Application

DBAL is intentionally easy to test against. The package's own suite is 70 tests / 113 assertions running in under 100ms against SQLite in-memory. You can borrow the same patterns for your application tests.

The simplest pattern — SQLite in-memory

use InitORM\DBAL\Connection\Connection;
use PHPUnit\Framework\TestCase;

abstract class DbTestCase extends TestCase
{
    protected Connection $db;

    protected function setUp(): void
    {
        $this->db = new Connection([
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'charset'  => '',
        ]);
        $this->migrate();
        $this->seed();
    }

    private function migrate(): void
    {
        $this->db->getPDO()->exec('CREATE TABLE users (
            id    INTEGER PRIMARY KEY AUTOINCREMENT,
            name  TEXT NOT NULL,
            email TEXT NOT NULL,
            active INTEGER NOT NULL DEFAULT 1
        )');
    }

    private function seed(): void
    {
        $this->db->query(
            "INSERT INTO users (name, email) VALUES (:n, :e)",
            ['n' => 'Alice', 'e' => 'alice@example.com']
        );
    }
}

Every test gets a clean, isolated database. No fixtures to clean up, no ordering concerns.

SQLite vs MySQL/PostgreSQL — what to watch for

SQLite is fast and convenient, but it is not a drop-in equivalent for the database you ship against. Differences that bite:

Behaviour SQLite MySQL PostgreSQL
numRows() on SELECT unreliable OK with buffered OK with buffered
Boolean columns INTEGER 0/1 TINYINT 0/1 TRUE/FALSE
Date / time types TEXT DATETIME etc. TIMESTAMP etc.
Transactional DDL yes implicit commit yes
String vs blob comparisons dynamic type strict strict
LIMIT … OFFSET … precedence OK OK requires both

For anything that touches these — boolean filters, date arithmetic, multi-statement migrations — run your tests against the real database in CI as well. The library's own CI uses SQLite for unit tests; integration suites against real engines belong in your application.

Testing repositories

If you wrap DBAL in a repository, type-hint the interface:

use InitORM\DBAL\Connection\Interfaces\ConnectionInterface;

final class UserRepository
{
    public function __construct(private ConnectionInterface $db) {}

    public function findById(int $id): ?array
    {
        return $this->db->query(
            'SELECT id, name, email FROM users WHERE id = :id',
            ['id' => $id]
        )->asAssoc()->row();
    }
}

…and inject a real, SQLite-backed Connection in the test:

final class UserRepositoryTest extends DbTestCase
{
    public function test_finds_existing_user(): void
    {
        $repo = new UserRepository($this->db);

        $user = $repo->findById(1);
        self::assertSame('Alice', $user['name']);
    }

    public function test_returns_null_for_missing_user(): void
    {
        $repo = new UserRepository($this->db);
        self::assertNull($repo->findById(9999));
    }
}

There is rarely a reason to mock Connection — the SQLite path is already fast and exercises real PDO behaviour. Mocks tend to hide binding bugs; the in-memory database catches them.

Capturing queries during a test

public function test_repository_runs_expected_queries(): void
{
    $this->db->setQueryLogs(true);

    (new UserRepository($this->db))->findById(1);

    $queries = array_column($this->db->getQueryLogs(), 'query');
    self::assertContains('SELECT id, name, email FROM users WHERE id = :id', $queries);
}

Asserting on the failure log

If you need to verify that a path triggers a log message, plug a callable in as the sink:

$messages = [];
$db = new Connection([
    'driver'   => 'sqlite',
    'database' => ':memory:',
    'charset'  => '',
    'log'      => static function (string $m) use (&$messages): void {
        $messages[] = $m;
    },
]);

try {
    $db->query('SELECT * FROM nope');
} catch (\Throwable $e) {
    // expected
}

self::assertNotEmpty($messages);
self::assertStringContainsString('SELECT * FROM nope', $messages[0]);

Mocking the data mapper factory

For corner cases where you want every prepared statement to be intercepted, swap in a custom factory through the second constructor argument:

use InitORM\DBAL\DataMapper\Interfaces\DataMapperFactoryInterface;
use InitORM\DBAL\DataMapper\Interfaces\DataMapperInterface;

final class SpyFactory implements DataMapperFactoryInterface
{
    public array $prepared = [];

    public function createDataMapper(\PDOStatement $stmt): DataMapperInterface
    {
        $this->prepared[] = $stmt->queryString;
        return new \InitORM\DBAL\DataMapper\DataMapper($stmt);
    }
}

$spy = new SpyFactory();
$db  = new Connection($credentials, $spy);

(new UserRepository($db))->findById(1);

self::assertContains(
    'SELECT id, name, email FROM users WHERE id = :id',
    $spy->prepared
);

Speed tips

  • Use :memory: SQLite (~0.1ms per test setup) rather than a temp file.
  • Share the schema-creation step across tests with PHPUnit's setUpBeforeClass — but recreate Connection per test so state is isolated.
  • Disable query logging in setup-heavy tests; it has a small but real per-call cost.

What's next

Clone this wiki locally