feat: use testcontainers for isolated MySQL in test runs (#153)
## Summary - Add `testcontainers/testcontainers` to `require-dev` to spin up an ephemeral MySQL 8.0 container per test process, eliminating the need for a local database or Docker Compose stack - Create `tests/bootstrap.php` that starts a `MySQLContainer`, sets `DB_*` env vars dynamically, and auto-generates `APP_KEY` when missing (supports fresh worktrees and CI) - Update `phpunit.xml` to use the new bootstrap file and remove the hardcoded `DB_DATABASE` env var ## How it works Running `php artisan test` now automatically starts a short-lived MySQL container on a random port. Each test process gets its own isolated database, so multiple agents, worktrees, or CI jobs can run in parallel without conflicts. Set `TESTCONTAINERS=false` to bypass containers and use the database configured in `.env` instead.
This commit is contained in:
parent
f2a7f955e6
commit
e4243c2eaa
|
|
@ -60,6 +60,7 @@ jobs:
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: ./vendor/bin/pest --exclude-testsuite=Browser,Performance
|
run: ./vendor/bin/pest --exclude-testsuite=Browser,Performance
|
||||||
env:
|
env:
|
||||||
|
TESTCONTAINERS: false
|
||||||
DB_CONNECTION: mysql
|
DB_CONNECTION: mysql
|
||||||
DB_HOST: 127.0.0.1
|
DB_HOST: 127.0.0.1
|
||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
|
|
@ -118,6 +119,7 @@ jobs:
|
||||||
- name: Browser Tests
|
- name: Browser Tests
|
||||||
run: ./vendor/bin/pest --testsuite=Browser --ci
|
run: ./vendor/bin/pest --testsuite=Browser --ci
|
||||||
env:
|
env:
|
||||||
|
TESTCONTAINERS: false
|
||||||
DB_CONNECTION: mysql
|
DB_CONNECTION: mysql
|
||||||
DB_HOST: 127.0.0.1
|
DB_HOST: 127.0.0.1
|
||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
|
|
@ -197,6 +199,7 @@ jobs:
|
||||||
- name: Performance Tests
|
- name: Performance Tests
|
||||||
run: ./vendor/bin/pest --testsuite=Performance
|
run: ./vendor/bin/pest --testsuite=Performance
|
||||||
env:
|
env:
|
||||||
|
TESTCONTAINERS: false
|
||||||
DB_CONNECTION: mysql
|
DB_CONNECTION: mysql
|
||||||
DB_HOST: 127.0.0.1
|
DB_HOST: 127.0.0.1
|
||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"pestphp/pest": "^4.1",
|
"pestphp/pest": "^4.1",
|
||||||
"pestphp/pest-plugin-browser": "^4.0",
|
"pestphp/pest-plugin-browser": "^4.0",
|
||||||
"pestphp/pest-plugin-laravel": "^4.0"
|
"pestphp/pest-plugin-laravel": "^4.0",
|
||||||
|
"testcontainers/testcontainers": "^1.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
bootstrap="vendor/autoload.php"
|
bootstrap="tests/bootstrap.php"
|
||||||
colors="true"
|
colors="true"
|
||||||
>
|
>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
|
|
@ -29,7 +29,6 @@
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
<env name="CACHE_STORE" value="array"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
<env name="DB_DATABASE" value="testing"/>
|
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Testcontainers\Modules\MySQLContainer;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Test Bootstrap — Testcontainers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This bootstrap starts an ephemeral MySQL container before any tests run.
|
||||||
|
| Each test process gets its own isolated database on a random port, so
|
||||||
|
| multiple agents, worktrees, or CI jobs can run tests in parallel
|
||||||
|
| without conflicts.
|
||||||
|
|
|
||||||
|
| Set TESTCONTAINERS=false to skip container creation and use the
|
||||||
|
| database configured in your .env file instead.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$useContainers = filter_var(
|
||||||
|
$_SERVER['TESTCONTAINERS'] ?? $_ENV['TESTCONTAINERS'] ?? 'true',
|
||||||
|
FILTER_VALIDATE_BOOLEAN,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Autoload must be available before anything else
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Create a minimal .env file when none exists so PHPDotenv does not
|
||||||
|
// emit a file_get_contents warning on every single test.
|
||||||
|
$envPath = __DIR__.'/../.env';
|
||||||
|
if (! file_exists($envPath)) {
|
||||||
|
file_put_contents($envPath, "APP_ENV=testing\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($useContainers) {
|
||||||
|
$container = (new MySQLContainer('8.0'))
|
||||||
|
->withMySQLDatabase('testing')
|
||||||
|
->withMySQLUser('testing', 'testing')
|
||||||
|
->start();
|
||||||
|
|
||||||
|
// Stop and remove the container when the PHP process exits, even on
|
||||||
|
// crashes or SIGINT. testcontainers-php has no built-in cleanup.
|
||||||
|
register_shutdown_function(function () use ($container): void {
|
||||||
|
$container->stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
$host = $container->getHost();
|
||||||
|
$port = (string) $container->getFirstMappedPort();
|
||||||
|
|
||||||
|
// Set environment variables before Laravel boots so config/database.php
|
||||||
|
// reads the dynamic host and port via env(). Both putenv() and $_ENV/$_SERVER
|
||||||
|
// are needed because Laravel's env() helper checks multiple sources.
|
||||||
|
$env = [
|
||||||
|
'DB_CONNECTION' => 'mysql',
|
||||||
|
'DB_HOST' => $host,
|
||||||
|
'DB_PORT' => $port,
|
||||||
|
'DB_DATABASE' => 'testing',
|
||||||
|
'DB_USERNAME' => 'testing',
|
||||||
|
'DB_PASSWORD' => 'testing',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate an APP_KEY if one is not already set, so tests can run
|
||||||
|
// without a .env file (e.g. fresh worktrees, CI environments).
|
||||||
|
if (empty($_SERVER['APP_KEY'] ?? $_ENV['APP_KEY'] ?? getenv('APP_KEY'))) {
|
||||||
|
$env['APP_KEY'] = 'base64:'.base64_encode(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($env as $key => $value) {
|
||||||
|
putenv("{$key}={$value}");
|
||||||
|
$_ENV[$key] = $value;
|
||||||
|
$_SERVER[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue