Tests
La suite PHPUnit s'exécute contre un vrai PostgreSQL jetable, identique au moteur de production, et non contre SQLite. Cela détecte les écarts de dialecte (type uuid natif, sémantique des contraintes et des transactions, fonctions spécifiques) invisibles sous SQLite.
Configuration PHPUnit
phpunit.xml impose PostgreSQL par défaut :
DB_CONNECTION=pgsqletDB_DATABASE=testingsont déclarés avecforce="true". C'est un garde-fou : unphp artisan testlancé par mégarde ne peut jamais être repointé vers la base de dev/prod par une variable d'environnement héritée (le conteneur de dev imposeDB_DATABASE=backend) — il échoue bruyamment au lieu de détruire des données. L'hôte (DB_HOST) et les identifiants restent surchargeables pour que chaquecomposefournisse les siens.- Deux suites :
Unit(tests/Unit) etFeature(tests/Feature). La couverture est mesurée surapp/. - Environnement de test :
CACHE_STORE=array,QUEUE_CONNECTION=sync,SESSION_DRIVER=array,MAIL_MAILER=array,BCRYPT_ROUNDS=4.
Le schéma est géré par les traits RefreshDatabase / LazilyRefreshDatabase, qui exécutent migrate:fresh sur la base jetable. Avec LazilyRefreshDatabase, seuls les tests touchant réellement la base déclenchent la migration.
Pile de test jetable
docker-compose.test.yml fournit deux services :
db-test(postgis/postgis:17-3.5, même image qu'en CI et en dev — la recherche par proximité a besoin de l'extension) — données surtmpfs(en RAM, aucun volume nommé), donc chaque exécution part d'une base vierge et ne laisse rien derrière elle. La durabilité est désactivée (fsync=off,full_page_writes=off,synchronous_commit=off), inutile pour une base éphémère, ce qui accélèremigrate:freshet les transactions. Healthcheck viapg_isready. Port5435:5432(évite le conflit avec la base de dev).test— le runner PHP. Son entrypoint (docker/php/test-entrypoint.sh) attend que la base réponde, vide la config en cache (config:clear— sinon une connexion figée dans l'image serait réutilisée), puis lance la suite. Il ne migre pas lui-même : ce sont les traits qui s'en chargent.
Lancement — bin/test
Le script hôte bin/test orchestre le cycle complet :
- construit l'image
test; - démarre
db-test(attente du healthcheck viadepends_on) ; - exécute la suite (
docker compose run --rm -T,-Tpour un comportement non interactif identique à la CI) ; - démonte tout (
down --volumes --remove-orphans) — garanti même en cas d'échec ou d'interruption, grâce à untrap EXIT.
Les arguments sont transmis à PHPUnit : bin/test --filter=User.
Exécution parallèle
La CI lance la suite avec --parallel (ParaTest, brianium/paratest), et bin/test --parallel fait la même chose en local. Mesuré sur la suite complète (1401 tests, 5392 assertions) : 77,5 s en séquentiel contre 30,8 s sur 4 processus.
Chaque worker reçoit sa propre base testing_test_{token}, créée puis migrée par les traits — PostGIS incluse, puisque l'extension est activée par une migration ordinaire (CREATE EXTENSION IF NOT EXISTS postgis) et non par un template de base. Aucune préparation particulière n'est donc nécessaire.
Deux points à connaître :
- ParaTest répartit par fichier, pas par méthode. La durée minimale de la suite est donc celle du fichier le plus long, et scinder ce fichier rapporte plus que d'ajouter un worker. Mesuré via le rapport JUnit : 131,4 s de temps cumulé sur 146 classes, dominées par
OpenApiExportTest(19,7 s — 12 exports Scramble complets) etDatabaseSeederTest(16,0 s). Le plancher est donc ~19,7 s : à 4 workers l'idéal théorique est 32,8 s (mesuré : 34,4 s), et au-delà de ~7 workers le fichier le plus long devient la contrainte. - Une classe sans trait de base de données ne bascule pas sur la base du worker :
ParallelTesting::setUpTestCasen'appelleswitchToDatabaseque pour les classes utilisantRefreshDatabase/LazilyRefreshDatabase/DatabaseTransactions/DatabaseTruncation. Elle reste donc sur la base racinetesting, que rien ne migre. C'est sans conséquence pour les classes actuellement concernées (tests/Feature/Contract/), qui n'ont pas besoin de schéma — vérifié : elles passent contre une base vierge. Mais une classe sans trait qui lirait la base serait silencieusement fausse plutôt qu'en erreur, PostgreSQL renvoyant une liste de colonnes vide au lieu de lever.
Le nombre de processus doit rester explicite en CI (TEST_PROCESSES) : nproc dans un conteneur rapporte les cœurs de l'hôte et ignore un quota CFS.
Arborescence des tests
tests/
├── TestCase.php
├── Unit/
│ └── Enums/ # MembershipStatus, OrganizationType, UserStatus
└── Feature/
├── Console/ # EnsureSuperAdmin, Probe
├── Models/ # User, Organization, Membership
└── Permissions/ # ScopedRole, SuperAdminGatePoints de vigilance
- Plus lent que SQLite en mémoire (build d'image + démarrage de PostgreSQL) ; c'est le compromis assumé pour tester sur le vrai moteur.
php artisan testexige désormais un PostgreSQL accessible exposant une basetesting: le chemin nominal estbin/test. Lancé hors de cette pile, il échoue (par sécurité) au lieu de retomber sur SQLite.- Toute config Laravel mise en cache dans l'image figerait l'ancienne connexion — d'où le
config:clearde l'entrypoint.

