Skip to content

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=pgsql et DB_DATABASE=testing sont déclarés avec force="true". C'est un garde-fou : un php artisan test lancé 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 impose DB_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 chaque compose fournisse les siens.
  • Deux suites : Unit (tests/Unit) et Feature (tests/Feature). La couverture est mesurée sur app/.
  • 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 sur tmpfs (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ère migrate:fresh et les transactions. Healthcheck via pg_isready. Port 5435: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 :

  1. construit l'image test ;
  2. démarre db-test (attente du healthcheck via depends_on) ;
  3. exécute la suite (docker compose run --rm -T, -T pour un comportement non interactif identique à la CI) ;
  4. démonte tout (down --volumes --remove-orphans) — garanti même en cas d'échec ou d'interruption, grâce à un trap 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) et DatabaseSeederTest (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::setUpTestCase n'appelle switchToDatabase que pour les classes utilisant RefreshDatabase / LazilyRefreshDatabase / DatabaseTransactions / DatabaseTruncation. Elle reste donc sur la base racine testing, 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, SuperAdminGate

Points 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 test exige désormais un PostgreSQL accessible exposant une base testing : le chemin nominal est bin/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:clear de l'entrypoint.

Contributors

No contributors

Changelog

No recent changes