---
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 |

<Callout title="Pourquoi le compromis fonctionne" type="info">
  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.
</Callout>
