othmanhaba/laravel-test-container

Run your Laravel test suite in parallel against a disposable MySQL or PostgreSQL container.

Maintainers

Package info

github.com/OthmanHaba/laravel-test-container

pkg:composer/othmanhaba/laravel-test-container

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-08-16 14:03 UTC

This package is auto-updated.

Last update: 2026-08-16 14:09:23 UTC


README

Run your Laravel test suite in parallel against a disposable MySQL or PostgreSQL container. Boots the database, runs the tests, throws the container away.

php artisan test:container
  Starting mysql:8.4 ...
  Ready on 127.0.0.1:32771

   PASS  Tests\Feature\UserTest
  ✓ it registers a user

  Tests:  42 passed (108 assertions)

No container left behind — including when you hit Ctrl-C.

Install

composer require --dev othmanhaba/laravel-test-container

Docker must be running. That's the whole setup — the config file is optional.

Usage

Every option it doesn't recognise is forwarded to artisan test, so what you already know keeps working:

php artisan test:container --filter=UserTest
php artisan test:container -p 8
php artisan test:container --driver=pgsql
php artisan test:container --tag=8.0

Its own options:

Option Effect
--driver= mysql or pgsql. Defaults to config.
--tag= Image tag, e.g. --tag=8.0. Defaults to mysql:8.4 / postgres:16.
--reuse Keep the container alive for the next run.
--no-parallel Single process, for when parallel output makes debugging hard.
--skip-container-check Skip the post-run check described below.

Publish the config if you want different defaults:

php artisan vendor:publish --tag=test-container-config

How it works

One container, many databases. Laravel's --parallel mode already gives each worker its own schema — testing_test_1, testing_test_2, and so on — created and migrated for you. So this package boots one database server and lets Laravel do the rest. Eight containers for eight workers would cost a minute of boot time and several gigabytes of RAM for no isolation you don't already have.

Because Laravel creates those databases at runtime, the container's superuser is used. Don't swap it for a restricted account; parallel mode needs CREATE DATABASE.

The connection is injected through a service provider, not DB_* env vars. This is the part that isn't obvious. A typical phpunit.xml contains:

<env name="DB_CONNECTION" value="sqlite" force="true"/>

With force="true", PHPUnit overwrites any environment variable we export. The suite then runs green against in-memory SQLite while a MySQL container sits idle next to it — passing, and testing nothing you booted. So the connection is applied in a service provider instead, which registers when the app boots inside each worker, after PHPUnit is done setting env vars. It also layers over cached config, so config:cache is harmless.

And it's verified afterwards. Once the suite finishes, the command checks that worker databases actually exist in the container. If nothing was written, it fails with a pointer at your phpunit.xml rather than reporting a green run.

Disposable means disposable. The data directory is a tmpfs and durability is turned off (innodb-flush-log-at-trx-commit=0, fsync=off), which is free speed when the data is discarded regardless.

The host port is chosen by Docker, never guessed. The container is published on port 0, which means "allocate a free port and bind it" in one atomic step. Your locally-installed MySQL keeps 3306, your Postgres keeps 5432, other projects' containers keep theirs, and two runs of this command at the same time cannot collide. Nothing probes a port and hopes it is still free a moment later.

Cleanup

Situation Handled by
Tests finish, pass or fail finally
Uncaught exception finally
Ctrl-C, kill Signal handler
PHP fatal error Shutdown backstop
kill -9, machine loses power Nothing — run test:container:prune
php artisan test:container:prune

Removes anything this package left running. Also the reset button for a stale --reuse container.

--reuse

Skips the ~8s boot by keeping the container between runs:

php artisan test:container --reuse

The trade-off is real and worth stating: state survives from the previous run. If you change migrations while using --reuse, expect confusing failures. test:container:prune clears it.

Requirements

  • PHP 8.2+
  • Laravel 11 or 12
  • Docker

On Windows, pcntl is unavailable, so Ctrl-C won't clean up. test:container:prune covers it.

Testing this package

composer install
vendor/bin/phpunit --testsuite=Unit          # fast, no Docker
vendor/bin/phpunit --testsuite=Integration   # boots a real container

License

MIT