Files
_Assistant_Lead_Tech/knowledge/backend/patterns/tests.md
T
MaksTinyWorkshop f1b783407a docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md
(lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation
remote antérieure (triage 2026-05-02).

~73 entrées intégrées sur knowledge/backend/, dont :
- patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist
- patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C),
  row réactivable, endpoint replace atomique, updateMany conditionnel, etc.
- risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste,
  fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.)
- patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests
- compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...)
- README patterns/risques mis à jour

Doublons internes corrigés en relecture (suppression-champ .map() → general seul ;
e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés.
Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:25:02 +02:00

533 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Backend — Patterns : Tests
domain: backend
bucket: patterns
tags: [tests, vitest, prisma, integration, isolation]
applies_to: [implementation, review, debug]
severity: high
validated_on: 2026-05-02
source_projects: [RL799_V2]
---
# Backend — Patterns : Tests
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<a id="pattern-cleanup-track-tests-integration-db"></a>
## Pattern : `cleanup.track()` LIFO pour tests d'intégration DB
- Objectif : nettoyer proprement les artefacts créés par chaque test sans recourir à un `TRUNCATE CASCADE` qui casserait les transactions du SUT.
- Contexte : tests qui écrivent dans une vraie base (pas mockée), en cohabitation avec un seed durable et des tables FK-liées.
- Quand l'utiliser : dès qu'un test crée des rows à la volée et qu'on veut éviter la pollution inter-tests.
- Quand l'éviter : si le projet utilise `transactions + rollback par test` (déjà isolé), ou si le SUT n'écrit jamais en DB.
- Avantage :
- ordre LIFO = ordre FK-safe (les enfants tombent avant les parents)
- résistant aux throws intermédiaires (le `afterEach` tourne quand même)
- pas de `TRUNCATE` qui casserait les transactions ouvertes par le SUT
- Limites / vigilance :
- convention "100 % via `cleanup.track`" : ne pas mélanger avec des `prisma.*.deleteMany` directs
- la queue est pour le teardown, pas pour le setup : ne jamais tracker une création
- Validé le : 24-04-2026
- Contexte technique : Vitest / Prisma / Postgres — RL799_V2
### Implémentation (helper minimal)
```typescript
// __tests__/helpers/db.ts
type CleanupFn = () => Promise<void> | void;
export function createCleanup() {
const queue: CleanupFn[] = [];
return {
track(fn: CleanupFn) { queue.push(fn); },
async run() {
while (queue.length > 0) {
const fn = queue.pop()!;
try { await fn(); }
catch (err) { console.warn('cleanup.run error:', err); }
}
},
};
}
```
### Usage typique
```typescript
const cleanup = createCleanup();
afterEach(async () => { await cleanup.run(); });
test('crée une tenue', async () => {
const tenue = await prisma.tenue.create({ data: { /* … */ } });
cleanup.track(async () => {
await prisma.tenue.deleteMany({ where: { id: tenue.id } });
});
// … assertions
});
```
### Checklist
- [ ] Ordre LIFO respecté (FK-safe)
- [ ] Aucun `prisma.*.deleteMany` direct hors `cleanup.track` dans les fichiers concernés
- [ ] Cleanup défensif (`try/catch + warn`) — pas de partial leak
- [ ] Aucune création (seulement des suppressions/restores) dans la queue
---
<a id="pattern-globalsetup-vitest-purge-residus"></a>
## Pattern : `globalSetup` vitest pour purger les résidus DB inter-runs
- Objectif : repartir d'une DB propre en début de suite quand des cleanups intra-test sont incomplets et que les artefacts s'accumulent entre sessions.
- Contexte : projet vitest avec `maxWorkers: 1` (DB partagée), tables 100 % alimentées par les tests (notifications, audit logs, payments éphémères).
- Quand l'utiliser : symptômes de "tests verts en isolation, rouges en suite complète après plusieurs jours" liés à des `findFirst({ type })` qui tombent sur des résidus.
- Quand l'éviter : si la suite tourne déjà avec une stratégie `transactions + rollback` ou DB-per-worker.
- Avantage :
- hook standard vitest, pas de cron externe ni de `prisma:reset` manuel entre sessions
- 1 seule passe au démarrage de la suite, coût négligeable
- Limites / vigilance :
- **NE PAS purger les tables seed** (users, soirees seed, profiles, etc.) — réservé aux tables 100 % "test-only"
- ne corrige pas la flakiness intra-run entre fichiers consécutifs
- Validé le : 25-04-2026
- Contexte technique : Vitest / Prisma — RL799_V2
### Implémentation
```typescript
// vitest.config.ts
test: {
maxWorkers: 1,
globalSetup: ['./src/__tests__/globalSetup.ts'],
}
// src/__tests__/globalSetup.ts
export default async function globalSetup() {
await prisma.notification.deleteMany();
await prisma.auditLog.deleteMany();
await prisma.cotisationPayment.deleteMany();
await prisma.$disconnect();
}
```
### Vérification avant d'ajouter une table
```bash
# Tables référencées par un snapshot/seed → NE PAS purger
grep -rln "prisma.<entity>.findMany\|seedSnapshot" src/__tests__/
```
### Checklist
- [ ] Aucune table seed dans la liste des `deleteMany`
- [ ] Hook référencé dans `vitest.config.ts`
- [ ] Justifier en commentaire pourquoi chaque table listée est test-only
---
<a id="pattern-template-database-isolation-fichiers"></a>
## Pattern : Template database Postgres pour isoler les fichiers de tests
- Objectif : éliminer la flakiness inter-fichiers en garantissant que chaque fichier de tests démarre avec une DB seedée pristine.
- Contexte : suite vitest qui partage une DB Postgres unique (cas typique : `maxWorkers: 1` ou DB unique côté CI).
- Quand l'utiliser : projet avec ≥50 fichiers de tests partageant une DB, présence de tests qui mutent les données seed sans restaurer systématiquement.
- Quand l'éviter : si les tests utilisent transactions+rollback (déjà isolés), DB-per-worker, ou si le rôle Postgres n'a pas le droit `CREATEDB`.
- Avantage :
- flakiness inter-fichiers éliminée par construction
- les tests ne polluent plus la DB de dev partagée
- réutilisation entre sessions : ~27 s au 1er run, < 1 s aux suivants
- Limites / vigilance :
- surcoût ~50 s sur 134 fichiers (cycle `drop + create FROM TEMPLATE` ~380 ms par fichier)
- les sub-processes (`prisma db seed`) doivent recevoir les envs critiques explicitement (`APP_BASE_URL`, `JWT_SECRET`, `ENCRYPTION_KEY`)
- Validé le : 01-05-2026
- Contexte technique : Prisma 7 / pg 8 / Postgres 17 / vitest 4 — RL799_V2
### Mécanique en 3 lots
**Lot 1 — DSN + primitives admin SQL** (`dbUrls.ts`, `dbAdmin.ts`) : `getAdminDsn()`, `getTemplateDsn()`, `getTestDsn()`, `databaseExists`, `dropDatabase` (`pg_terminate_backend` + `DROP IF EXISTS`), `createDatabase(name, { template })`. Whitelist stricte des noms de DB (les SQL admin ne supportent pas `$1`).
**Lot 2 — Bootstrap idempotent du template** (`bootstrapTemplate.ts`) :
```typescript
export async function ensureTemplateReady() {
if (await databaseExists(TEMPLATE)) return; // no-op si déjà là
await createDatabase(TEMPLATE);
spawnSync('pnpm', ['-C', 'apps/api', 'exec', 'prisma', 'migrate', 'deploy'], {
env: { ...process.env, DB_URL: getTemplateDsn() },
});
spawnSync('pnpm', ['-C', 'apps/api', 'exec', 'prisma', 'db', 'seed'], {
env: { ...process.env, DB_URL: getTemplateDsn() },
});
}
```
**Lot 3 — Câblage vitest** :
```typescript
// globalSetup.ts
export default async function globalSetup() {
await ensureTemplateReady();
if (await databaseExists(TEST)) await dropDatabase(TEST);
return async () => {
if (await databaseExists(TEST)) await dropDatabase(TEST);
};
}
// setupFile.ts (avant chaque fichier)
if (await databaseExists(TEST)) await dropDatabase(TEST);
await createDatabase(TEST, { template: TEMPLATE });
```
### Pièges anticipés
- **`resolveDbUrl()` qui force le pathname `/test`** : doit préserver le DSN s'il pointe déjà sur la template (sinon le seed sub-process écrit dans la mauvaise DB).
- **`prisma db seed` sub-process** : les envs critiques chargées par `lib/prisma.ts` côté app doivent être propagées au sub-process (`loadEnv.ts` importé en tête de `globalSetup.ts`).
- **`globalForPrisma` cache un PrismaClient** : c'est le **contenu** de la DB qui change (drop+create), pas le DSN — pas de `$disconnect()` nécessaire grâce à `pg_terminate_backend`.
### Checklist
- [ ] Whitelist stricte des noms de DB autorisés (anti-injection)
- [ ] Bootstrap idempotent (réutilise la template entre sessions de dev)
- [ ] Sub-processes du bootstrap reçoivent les envs critiques
- [ ] Drop + create FROM TEMPLATE au début de chaque fichier
- [ ] Test de flakiness avant/après pour valider le gain
---
<a id="pattern-helper-waitfor-fire-and-forget"></a>
## Pattern : Helper `waitForX()` polling-borné pour les attentes fire-and-forget
- Objectif : remplacer les `setTimeout(50) + findFirst` dispersés par un helper centralisé robuste à la charge CPU et auto-documenté.
- Contexte : tests qui valident un side-effect async (audit log, notification, mail log) déclenché par `setImmediate` / `Promise.resolve().then(...)` côté SUT.
- Quand l'utiliser : tout test qui attend qu'un side-effect non-awaited apparaisse en DB.
- Quand l'éviter : pour vérifier l'**absence** d'un event — là il faut un délai fixe puis assertion d'absence (le polling jusqu'au timeout ne prouve rien).
- Avantage :
- robuste aux variations de charge (pas de durée arbitraire)
- fail rapide si l'event n'arrive pas (timeout 1500 ms par défaut)
- lisibilité : intention claire (`waitForX` vs `setTimeout` + `findFirst`)
- Limites / vigilance :
- le polling consomme des requêtes DB (1 toutes les 50 ms) — négligeable en `maxWorkers: 1`
- **NE PAS** utiliser pour vérifier l'absence d'un event
- Validé le : 25-04-2026
- Contexte technique : Vitest / Prisma — RL799_V2
### Implémentation
```typescript
// __tests__/helpers/asyncWait.ts
export const waitForAudit = async (
query: { action: string; targetId?: string; userId?: string },
options: { timeoutMs?: number; intervalMs?: number } = {},
) => {
const timeout = options.timeoutMs ?? 1500;
const interval = options.intervalMs ?? 50;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const audit = await prisma.auditLog.findFirst({ where: query });
if (audit) return audit;
await new Promise((r) => setTimeout(r, interval));
}
return null;
};
```
### Anti-pattern
```typescript
// ❌ délai arbitraire et fragile
await new Promise((r) => setTimeout(r, 50));
const notif = await prisma.notification.findFirst({ where: { ... } });
// ✅ polling borné, intention claire
const notif = await waitForNotification({ type: 'X', recipientId: userId });
```
### Checklist
- [ ] Filtre exhaustif (au minimum `recipientId` ou `targetId` pour éviter de matcher les artefacts d'un autre test)
- [ ] Timeout par défaut court (1500 ms)
- [ ] Migration progressive — pas tous les tests d'un coup
### Pattern symétrique pour vérifier l'ABSENCE d'un side-effect
`waitForX` ne convient pas pour prouver qu'**aucun** event n'apparaît : le polling jusqu'au timeout ne distingue pas "aucun event" de "event arrivé après le timeout". Le remplacer par `setTimeout(100) + count()` souffre d'une double fragilité (trop court en CI lent → faux négatif si l'event fuyant arrive après ; trop long → temps gaspillé).
Le helper symétrique correct est un polling-borné **fail-fast** (`assertCountStable`) : il poll-vérifie que le compteur reste à la valeur attendue sur une fenêtre courte, et abandonne **dès** qu'un compteur surnuméraire est observé.
```typescript
// __tests__/helpers/asyncWait.ts
export const assertNotificationCountStable = async (
query: { type: NotificationType; recipientId?: string },
expected: number,
options: { windowMs?: number; intervalMs?: number } = {},
): Promise<number> => {
const window = options.windowMs ?? 200;
const interval = options.intervalMs ?? 50;
const deadline = Date.now() + window;
let count = await prisma.notification.count({ where: query });
if (count > expected) return count; // fail-fast immédiat
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, interval));
count = await prisma.notification.count({ where: query });
if (count > expected) return count; // fail-fast
}
return count;
};
```
```typescript
// ❌ délai arbitraire — trop court en CI lent (faux négatif), trop long = temps perdu
await new Promise((r) => setTimeout(r, 100));
expect(await prisma.notification.count({ where: { /* … */ } })).toBe(1);
// ✅ polling borné, fail-fast si une notif fuyante apparaît
const count = await assertNotificationCountStable({ type: 'X', recipientId: userId }, 1);
expect(count).toBe(1);
```
Checklist (absence) :
- [ ] Fenêtre courte (200 ms par défaut — durée typique d'un fire-and-forget non-désiré, pas un timeout)
- [ ] Filtre exhaustif — `recipientId` ou `targetId` au minimum
- [ ] Le test affirme `count === expected` APRÈS le helper, pas pendant
Cas vécu : `PATCH/DELETE payments → aucune nouvelle notif` (`cotisationsPayments.test.ts`), 05-05-2026.
---
<a id="pattern-injection-dependance-hooks-module-level"></a>
## Pattern : Injection de dépendance testable via hooks module-level `__setXForTests` / `__resetXForTests`
- Objectif : surcharger en test une dépendance d'un module de prod (getter de clés JWKS, sub-resolver DB, …) SANS polluer la signature publique (param de DI) ni recourir à `vi.mock` (fragile sur les imports transitifs, hoisting capricieux).
- Contexte : module dont une dépendance interne doit varier en test mais dont la signature publique est un invariant ("signatures intouchables").
- Quand l'utiliser : la dépendance est interne et `vi.mock` se révèle fragile (chaîne d'imports transitifs).
- Quand l'éviter : la dépendance est déjà un paramètre explicite de la fonction (param-threading suffit) ; ou un `vi.mock` simple et stable fait l'affaire.
- Avantage :
- pas de param de DI dans le contrat public, pas de `vi.mock` fragile
- bonus diagnostic : injecter un fake qui **throw si appelé** prouve qu'un chemin n'est PAS pris (ex. "le sub-resolver Keycloak ne doit pas être appelé quand le JWT-maison gagne")
- Limites / vigilance :
- **reset systématique dans `setupFile.ts` avant chaque fichier** (même garde-fou que les rate-limiters in-memory : un fichier qui injecte ne doit pas polluer le suivant → flakiness inter-fichiers évitée)
- le défaut de PRODUCTION reste le comportement réel (ex. stub fail-closed `() => null` tant que la vraie impl n'existe pas)
- Validé le : 14-06-2026
- Contexte technique : Vitest — RL799_V2 (rate-limiters, puis Keycloak K1.1)
### Implémentation
```typescript
// module de prod
let active = productionDefault;
export function __setXForTests(v: typeof productionDefault): void { active = v; }
export function __resetXForTests(): void { active = productionDefault; }
```
```typescript
// setupFile.ts — reset AVANT chaque fichier
beforeEach(() => {
__resetXForTests();
__resetAllRateLimitersForTests();
});
```
Cas vécus : `__resetAllRateLimitersForTests` (`rateLimiter.ts`), puis `__setKeycloakKeyGetterForTests` / `__setSubResolverForTests` (K1.1).
---
<a id="pattern-e2e-db-based-nestjs-prisma-v7"></a>
## Pattern : e2e DB-based NestJS + Prisma v7 (Jest + @swc/jest)
- Objectif : exécuter des e2e Jest contre une vraie base Postgres (sans mocker `PrismaClient`) dans un projet NestJS utilisant Prisma v7.
- Contexte : Prisma v7 charge dynamiquement un runtime WASM (`await import('....mjs')`, `import.meta.url`) → incompatible avec la config Jest historique `ts-jest` + `moduleNameMapper` qui mock le client.
- Quand l'utiliser : projet NestJS + Prisma v7 voulant des e2e avec persistance réelle (validation de contracts Zod réels, fixtures partagées avec le seed, intégrité cross-modules).
- Quand l'éviter : suites qui n'ont pas besoin d'une vraie DB → garder le pipeline mocké (plus rapide, plus isolé).
- Avantage :
- exploite des builders typés contre une vraie base
- pattern inter-projet : tout NestJS + Prisma v7 rencontre le même problème
- Limites / vigilance :
- garde-fou anti-truncate destructeur obligatoire (refuser si `DATABASE_URL` ne contient pas "test")
- **PAS** de mock global `argon2` (sinon le hash est stub et les tests d'auth ne valident rien)
- Validé le : 27-05-2026
- Contexte technique : NestJS / Jest / @swc/jest / Prisma v7 / Postgres — app-alexandrie
### Config Jest e2e DB-based minimale
```json
{
"moduleFileExtensions": ["js", "mjs", "json", "ts"],
"testRegex": ".e2e-db-spec.ts$",
"setupFiles": ["<rootDir>/test-env-db.ts"],
"transformIgnorePatterns": ["/node_modules/(?!(@prisma)/)"],
"transform": {
"^.+\\.(t|j|mj)s$": ["@swc/jest", {
"jsc": {
"target": "es2023",
"parser": { "syntax": "typescript", "decorators": true, "dynamicImport": true },
"transform": { "legacyDecorator": true, "decoratorMetadata": true },
"keepClassNames": true
},
"module": { "type": "commonjs" }
}]
},
"moduleNameMapper": { "^(\\.{1,2}/.*)\\.js$": "$1" }
}
```
Points-clés :
- **`@swc/jest`** au lieu de `ts-jest` : gère `import.meta.url`, `await import()` dynamique, et respecte `decoratorMetadata` (DI Nest).
- **`transformIgnorePatterns`** ouvert pour `/node_modules/@prisma/` : les `.mjs` du runtime WASM doivent passer dans le transformer.
- **`moduleNameMapper`** strip les extensions `.js` : Prisma v7 écrit ses imports en ESM strict (`./internal/class.js`), Jest CJS résout `.ts`.
- **PAS** de `moduleNameMapper` mappant `PrismaClient` vers un mock (sinon on tape le mock, pas la vraie DB).
### Helper e2e avec garde-fou anti-truncate
```typescript
// _helpers/e2e-db.ts
export async function truncateE2EDb(prisma: PrismaClient): Promise<void> {
const url = process.env.DATABASE_URL ?? '';
if (!url.includes('test')) {
throw new Error('[e2e-db] truncate refusé : DATABASE_URL ne contient pas "test"');
}
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE;`);
}
```
Infra (à provisionner une fois) : DB dédiée avec "test" dans le nom + `DATABASE_URL=...test pnpm exec prisma migrate deploy`.
### Cohabitation avec les e2e mockés historiques
- Garder le pipeline mocké pour les suites sans besoin de vraie DB.
- Migrer progressivement vers le DB-based les e2e qui bénéficient d'une vraie persistance.
- Convention de nommage : `*.e2e-spec.ts` (mockés) vs `*.e2e-db-spec.ts` (DB-based) — extension distinctive captée par les `testRegex` respectifs.
---
<a id="pattern-test-atomicite-transaction"></a>
## Pattern : Tester l'atomicité d'une transaction (audit + mutation)
- Objectif : prouver par un test que la promesse "X est garanti par une transaction" tient — sans ce test, un refactor "fire-and-forget" passe en review sans alarme.
- Contexte : code de service qui revendique `mutation persistée ⇔ audit log existe` via `prisma.$transaction(async tx => { await tx.X.update(...); await logActionSync(tx, ...); })`.
- Quand l'utiliser : sur tout chemin critique où l'audit ou un side-effect transactionnel est un livrable métier.
- Quand l'éviter : pour les audits purement informatifs (statistiques d'usage) où le fire-and-forget est acceptable.
- Avantage :
- le test fait office de contrat exécutable du pattern transactionnel
- une régression silencieuse (passer `prisma` au lieu de `tx`) casse le test immédiatement
- Limites / vigilance :
- le mock doit cibler le seul appel ciblé (pas un mock global qui fait tout throw)
- Validé le : 27-04-2026
- Contexte technique : Vitest / Prisma — RL799_V2
### Implémentation (recipe vitest + spyOn)
```typescript
import * as auditRepository from '../repositories/admin/auditRepository';
test('mutation rollback si audit throw', async () => {
vi.spyOn(auditRepository, 'createAuditLog').mockImplementation(async (data) => {
if (data.action === 'X.cross_soiree_update') {
throw new Error('audit-failure-simulee');
}
return null as never;
});
const res = await POST_HANDLER(...);
assert.ok(res.status >= 500);
assert.equal(await prisma.X.findFirst(...), null); // rollback
assert.equal(await prisma.auditLog.count(...), 0);
});
```
### Checklist
- [ ] Mock chirurgical sur l'action ciblée, pas un mock global
- [ ] Assertions sur **trois** invariants : status HTTP 5xx, donnée non persistée, audit non écrit
- [ ] Restaurer le mock après le test (`vi.restoreAllMocks()` en `afterEach`)
---
<a id="pattern-describe-convention-tests-integration"></a>
## Pattern : Convention `describe()` minimale pour tests d'intégration
- Objectif : rendre les fichiers de tests > 150 LOC navigables, regroupables par cause d'échec et lisibles dans le reporter.
- Contexte : tests d'intégration API où les assertions s'enchaînent sans regroupement, ou fichiers historiquement plats.
- Quand l'utiliser : tout fichier > 150 LOC ou > 5 tests.
- Quand l'éviter : fichier court (< 50 LOC, < 3 tests) où le `describe` ajoute du bruit.
- Avantage :
- le reporter vitest groupe les échecs logiquement (toutes les erreurs d'auth ensemble)
- navigation facilitée par recherche (`describe('Auth'`)
- un groupe devient candidat naturel à l'extraction en fichier dédié
- Limites / vigilance :
- 2 niveaux maximum — au-delà ça devient illisible
- Validé le : 24-04-2026
- Contexte technique : Vitest — RL799_V2
### Convention recommandée (2 niveaux)
```typescript
describe('POST /api/tenues', () => {
describe('Auth', () => {
test('refuse un user non-admin → 403', async () => { /* … */ });
test('refuse un token invalide → 401', async () => { /* … */ });
});
describe('Validation', () => {
test('refuse un body sans date → 400', async () => { /* … */ });
});
describe('Happy path', () => {
test('crée une tenue valide → 201', async () => { /* … */ });
});
});
```
### Seuils
- < 50 LOC et < 3 tests : optionnel
- 50150 LOC : 1 niveau de contexte
- \> 150 LOC ou > 5 tests : 2 niveaux obligatoires
### Niveau 2 — vocabulaire stable
`Auth`, `Validation`, `Happy path`, `Edge cases`, `Error handling`, `Integration`. Ne pas inventer un nom local — la cohérence inter-fichiers vaut plus que l'expressivité ponctuelle.
---
<a id="pattern-refactor-iteratif-tests-monolithe"></a>
## Pattern : Refactor itératif d'un fichier de tests monolithe
- Objectif : découper un fichier > 1000 LOC mêlant plusieurs domaines métier en fichiers thématiques sans introduire de régression.
- Contexte : fichier de tests historique qui partage un `beforeEach` lourd, n'a pas de `describe`, et accueille tous les nouveaux tests par défaut.
- Quand l'utiliser : > 1000 LOC, plusieurs domaines mélangés, navigation pénible.
- Quand l'éviter : fichier ciblé, déjà cohérent même volumineux.
- Avantage :
- 1 commit par domaine extrait → review possible commit par commit
- helpers réutilisables émergent naturellement
- reporter vitest groupe les échecs par domaine
- Limites / vigilance :
- flakiness inter-fichiers possible pendant la transition (cleanups incomplets temporaires)
- tolérer 1-2 fails au 1er run pendant le chantier, investiguer après
- Validé le : 25-04-2026
- Contexte technique : Vitest — RL799_V2
### Étapes (ordre obligatoire)
1. **Analyse** : `grep -nE "^test|^// ---" fichier.test.ts` → carte des sections + repérage des dépendances closure.
2. **Extraction des helpers d'abord** : `helpers/<domaine>.ts` avec `setup<Domaine>Fixtures(cleanup)` qui retourne explicitement les IDs (pas de closure module-level).
3. **POC sur le domaine le plus simple** (4-7 tests, peu de dépendances). Commit + push dès que vert.
4. **Itération domaine par domaine** par ordre croissant de complexité. Une seule règle : ne jamais commencer un nouveau domaine sans avoir commit le précédent.
5. **Suppression finale du fichier monolithe** quand le dernier domaine est extrait.
### Pièges à éviter
- **Ne jamais dupliquer le fichier** pour le scinder : toujours **déplacer** (extraction → suppression dans l'original → commit).
- **Closure variables partagées** : si les tests utilisent `let X = ''` au niveau module, les ids doivent passer par le retour de `setup<Domaine>`.
- **Snapshot/restore seed** : si le fichier original prenait un snapshot d'entries seed, **chaque** nouveau fichier doit le faire (sinon le 1er fichier qui tourne snapshot un état déjà pollué).
### Checklist
- [ ] Helpers extraits **avant** le 1er domaine
- [ ] 1 commit par domaine
- [ ] `wc -l` du fichier original baisse à chaque commit (preuve de progrès)
- [ ] Suite verte à chaque commit