Skip to main content
← All articles
testing

Testcontainers and the case against mocking your database

Testcontainers and the case against mocking your database

I have written a lot of tests that passed against a fake database and then exploded against a real one. That gap is the whole reason I keep coming back to real-dependency integration testing, and lately to Testcontainers.

If you have not met it: Testcontainers is an open source library for spinning up throwaway, lightweight instances of databases, message brokers, browsers, or “just about anything that can run in a Docker container.” Your test boots a real Postgres, MariaDB, or Redis in a container, runs against it, and the container is destroyed afterwards. It started in the Java world and now has implementations across roughly a dozen languages, including Go, Node, Python, .NET, Rust, and PHP.

The problem with mocks

Mocks are seductive because they are fast and they never flake. But a mock only tests your assumptions about a dependency, not the dependency. My MySQL mock does not know that a column is utf8mb4, that a UNIQUE constraint will fire, that Redis evicts keys under maxmemory, or that Drupal’s entity query builds SQL my in-memory double never sees.

A green test suite that mocks the database tells you your code agrees with your mocks. It says almost nothing about whether it agrees with MariaDB.

In Drupal specifically, this bites hard. So much behaviour lives in the storage layer: entity queries, field schemas, the cache backend, the lock system. Kernel tests already lean on a real database precisely because faking it is a losing game. Testcontainers is the same instinct, made portable and explicit.

How it actually works

The mechanics are refreshingly boring, which is a compliment. You need a Docker-compatible runtime. Your test defines a container, the library pulls the image and starts it, and a wait strategy holds your test until the service is genuinely ready (not just “process started”). Cleanup is handled by a sidecar container called Ryuk, which watches labelled resources and reaps containers, volumes, and networks even if your test process dies mid-run. No orphaned containers clogging your laptop the next morning.

  • Setup: start dependencies as containers, wire config to the mapped ports.
  • Execution: run tests against the real thing.
  • Teardown: Ryuk removes everything.

What testcontainers-php looks like

Here is the shape of it. The official testcontainers/testcontainers-php package requires PHP 8.1+ and ships modules for MySQL, MariaDB, Postgres, Redis, OpenSearch, plus a generic container:

use Testcontainers\Modules\PostgresContainer;

$container = (new PostgresContainer())
    ->withPostgresUser('bar')
    ->withPostgresDatabase('foo')
    ->start();

$pdo = new PDO(
    sprintf(
        'pgsql:host=%s;port=%d;dbname=foo',
        $container->getHost(),
        $container->getFirstMappedPort()
    ),
    'bar',
    'test',
);

Note getFirstMappedPort() — containers publish to a random host port, which is exactly what you want so parallel tests do not collide.

Honesty time: the PHP port is a community effort and far less mature than the Java or Go libraries. It appears to sit in the low hundreds of GitHub stars with a modest module list, and there are several competing forks (opencodeco, k-kinzal for legacy PHP, shyim’s package) rather than one obvious blessed choice. It is MIT-licensed like the core project. Useful and real, but do not expect the polish or ecosystem of Testcontainers for Java.

The commercial side, and the catches

AtomicJar, the company behind Testcontainers, was acquired by Docker in December 2023. The core library stays open source and MIT. Around it sit Testcontainers Desktop (free — fixed ports for debugging, freezing containers, switching runtimes) and Testcontainers Cloud, a paid service that runs the containers remotely, with runtime minutes bundled into Docker Pro/Team/Business tiers.

That Cloud offering exists because of the honest downsides. Real containers are slower than mocks — image pulls, boot time, wait strategies all add up. And Docker-in-CI is a genuine headache: Docker-in-Docker eats CPU and memory, does not share the host image cache, and produces its own flavour of flaky builds. Testcontainers Cloud is essentially Docker selling you a way out of that pain.

My take: use real dependencies where correctness against storage actually matters, keep them off the hot path of your fastest unit tests, and cache images aggressively in CI. For a Drupal shop, testcontainers-php is worth a spin for standalone services and libraries — just go in knowing it is the scrappy younger sibling.

Links

BM
Blue Moose
The moose behind Blue Moose. Full-stack PHP developer — Drupal by day, Symfony by night, tests always.