feat: docs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Tests",
|
||||
"description": "Lancer une infrastructure de test réelle et jetable",
|
||||
"pages": ["testcontainers"]
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user