feat: docs
CI / 🔍 Lint & Type Check (push) Successful in 2m11s
CI / 🐳 Build & Push Image (push) Successful in 1m16s

This commit is contained in:
2026-07-17 16:15:54 +02:00
parent 0c2f42b4a3
commit 1542a991b9
38 changed files with 6366 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"title": "Tests",
"description": "Lancer une infrastructure de test réelle et jetable",
"pages": ["testcontainers"]
}
+120
View File
@@ -0,0 +1,120 @@
---
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>