--- title: Tests e2e avec Testcontainers description: Tester contre une infrastructure réelle, isolée et détruite après chaque run. --- Docker permet aux tests de démarrer une infrastructure proche de la production, de l'utiliser, puis de la supprimer. ## Le problème des tests d'intégration Un backend qui utilise PostgreSQL, un stockage S3 et un serveur SMTP a besoin de ces dépendances pendant ses tests. Deux solutions classiques ont des limites : - **mocker les services** : rapide, mais ne teste pas le vrai SQL ni les vraies contraintes ; - **partager une base de test** : réaliste, mais l'état fuit entre les runs et les exécutions parallèles entrent en collision. Testcontainers donne à chaque suite ses propres conteneurs : une vraie base PostgreSQL, un vrai MinIO et un vrai Mailpit, isolés puis supprimés automatiquement. ## Niveau backend : Testcontainers en code ```ts const [pgContainer, mailpitContainer, minioContainer] = await Promise.all([ new PostgreSqlContainer("postgres:16-alpine") .withStartupTimeout(30_000) .start(), new GenericContainer("axllent/mailpit:latest") .withExposedPorts(1025, 8025) .start(), new GenericContainer("minio/minio:latest") .withCommand(["server", "/data"]) .withEnvironment({ MINIO_ROOT_USER: "minioadmin", MINIO_ROOT_PASSWORD: "minioadmin123", }) .withExposedPorts(9000) .start(), ]); process.env.POSTGRES_HOST = pgContainer.getHost(); process.env.POSTGRES_PORT = pgContainer.getPort().toString(); process.env.POSTGRES_USER = pgContainer.getUsername(); ``` L'application NestJS démarre ensuite contre ces services réels. ### Même image que la production Le test utilise `postgres:16-alpine`, comme la production. Il teste le vrai moteur et ses contraintes, pas une base en mémoire approximative. ### Ports dynamiques Testcontainers publie les ports internes sur des ports libres choisis automatiquement. `getPort()` renvoie le port attribué. Deux runs simultanés ne se gênent pas. ### Isolation par run Les données peuvent être nettoyées entre les tests, puis les conteneurs sont arrêtés à la fin. Chaque suite repart d'une infrastructure vierge. ## Le test reste orienté métier ```ts test("mes notifications sont triées par date décroissante", async () => { await driver.givenImAuthenticated(); await driver.givenIHaveNotifications(); await driver.whenIListMyNotifications(); await driver.thenMyNotificationsAreReturnedSortedDesc(); }); ``` Le test pilote l'application par HTTP comme un vrai client. L'infrastructure Docker reste cachée dans le setup. ## Niveau navigateur : une stack Compose par worker Les tests Playwright peuvent lancer la stack de production complète, accompagnée d'un overlay e2e : ```ts const ports = getWorkerPorts(workerInfo.workerIndex); execSync( "docker compose " + "--project-name " + ports.projectName + " " + "--env-file .env.prod " + "-f docker-compose.prod.yml " + "-f docker-compose.local-e2e.yml " + "up -d --build --force-recreate", { env: envOverride }, ); await use(ports); execSync( "docker compose --project-name " + ports.projectName + " down -v", ); ``` Trois notions rendent le parallélisme possible : 1. un `--project-name` unique donne à chaque worker son propre réseau ; 2. des ports hôte décalés évitent les collisions ; 3. l'overlay Compose réutilise la stack de production et ne modifie que la configuration de test. Plusieurs copies de la production peuvent ainsi tourner côte à côte dans la CI, puis être détruites avec leurs volumes. ## Comparaison | Critère | Mocks | Base partagée | Testcontainers | | --- | --- | --- | --- | | Vrai SQL et vraies contraintes | Non | Oui | Oui | | Proche de la production | Non | Partiellement | Oui, mêmes images | | Isolation entre les runs | Oui | Non | Oui | | Setup manuel | Non | Oui | Non, décrit en code ou Compose | | Parallélisable | Oui | Difficile | Oui, grâce aux ports et réseaux | La légèreté des conteneurs permet de démarrer l'infrastructure en quelques secondes. Les réseaux l'isolent, les ports dynamiques évitent les collisions et down -v la détruit proprement.