03 - Flaky tests : le cancer du pipeline
Ce que tu vas apprendre
- Ce qu'est un flaky test et pourquoi c'est dangereux
- Les causes les plus courantes
- Comment détecter et isoler les tests instables
- Comment corriger chaque type de flaky
Prerequisites
Avoir un pipeline CI en place (voir l'article 00 sur la CI/CD). Connaitre les bases des tests avec Bun.
Le test de Schrodinger
Mardi matin, le pipeline est rouge. Je regarde : un test d'envoi de mail a echoue. Je relance le pipeline sans rien changer. Il passe. Je relance encore. Il passe. Deux heures plus tard, il echoue a nouveau sur un autre push qui ne touche meme pas le code mail.
C'est un flaky test. Un test qui passe et echoue aleatoirement, sans modification du code. Le meme code, le meme test, des résultats différents.
Pourquoi c'est si dangereux
Le vrai danger d'un flaky test, c'est pas l'échec en soi. C'est la reaction de l'équipe. Apres trois faux positifs, les devs commencent a penser "c'est encore le test mail qui deconne". Ils relancent le pipeline mecaniquement. Et le jour ou un VRAI bug fait échouer le pipeline, ils relancent aussi sans regarder.
Sur paltemps.fr, on a eu une periode ou mes juniors relancaient chaque pipeline rouge sans meme lire le message d'erreur. Il a fallu corriger les flaky tests pour que le pipeline redevienne fiable, et que l'équipe recommence a prendre les échecs au serieux.
Un pipeline auquel personne ne fait confiance, autant ne pas en avoir.
Les causes les plus courantes
1. Timing et race conditions
Le classique. Ton test attend un résultat asynchrone mais n'attend pas assez longtemps. Ou il attend trop vite.
typescript// FLAKY : sleep arbitraire
it("sends notification", async () => {
await sendNotification(userId);
await Bun.sleep(100); // parfois 100ms suffisent, parfois non
const notifs = await getNotifications(userId);
expect(notifs).toHaveLength(1);
});
Le fix : utiliser du polling au lieu d'un sleep fixe.
typescript// STABLE : polling avec timeout
import { expect } from "bun:test";
it("sends notification", async () => {
await sendNotification(userId);
// Reessaie pendant 2 secondes max
let notifs: Notification[] = [];
const deadline = Date.now() + 2000;
while (Date.now() < deadline) {
notifs = await getNotifications(userId);
if (notifs.length > 0) break;
await Bun.sleep(50);
}
expect(notifs).toHaveLength(1);
});
2. État partage entre tests
Deux tests qui utilisent la meme table en base, le meme fichier temporaire, ou la meme variable globale. L'ordre d'exécution change le résultat.
typescript// FLAKY : les tests partagent la meme base
it("creates user", async () => {
await db.insert(users).values({ email: "test@test.com" });
const count = await db.select().from(users);
expect(count).toHaveLength(1); // FAIL si l'autre test a deja insere
});
it("lists users", async () => {
await db.insert(users).values({ email: "other@test.com" });
const all = await db.select().from(users);
expect(all).toHaveLength(1); // FAIL si l'autre test n'a pas nettoye
});
Le fix : nettoyer l'état avant chaque test.
typescript// STABLE : chaque test part d'une base propre
beforeEach(async () => {
await db.delete(users); // vider la table
});
it("creates user", async () => {
await db.insert(users).values({ email: "test@test.com" });
const count = await db.select().from(users);
expect(count).toHaveLength(1);
});
3. Dépendance a la date et l'heure
Un test qui passe a 14h mais echoue a 23h59 parce qu'il compare avec "aujourd'hui" et que le jour change entre le setup et l'assertion.
typescript// FLAKY : depend de l'heure reelle
it("formats today's date", () => {
const result = formatDate(new Date());
expect(result).toBe("2026-03-28"); // echoue demain
});
Le fix : mocker Date.now() ou passer une date fixe.
typescript// STABLE : date fixe
it("formats a specific date", () => {
const result = formatDate(new Date("2026-03-28T12:00:00Z"));
expect(result).toBe("2026-03-28");
});
4. Appels réseau a des APIs reelles
Un test qui appelle une vraie API externe. Le serveur est lent, indisponible, ou répond differemment.
typescript// FLAKY : depend d'un service externe
it("fetches weather", async () => {
const weather = await fetch("https://api.weather.com/paris");
const data = await weather.json();
expect(data.temperature).toBeDefined();
});
Le fix : utiliser un fake ou un mock. Jamais d'appel réseau réel dans les tests (sauf les e2e explicitement prevus pour ca). Voir l'article sur les mocks pour les techniques.
typescript// STABLE : mock du fetch
import { mock } from "bun:test";
it("fetches weather", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(new Response(JSON.stringify({ temperature: 22 })))
);
const weather = await getWeather("paris");
expect(weather.temperature).toBe(22);
});
5. Dépendance a l'ordre d'exécution
Un test qui marche seulement si un autre test a tourne avant lui. Souvent parce que le premier test créé des donnees que le second utilise.
Le fix : chaque test doit etre complètement independant. Si un test a besoin de donnees, il les créé dans son beforeEach. Si tu peux lancer un test seul avec bun test --filter "mon test" et qu'il passe, c'est bon.
Comment détecter les flaky tests
La méthode brute mais efficace : lancer les tests en boucle.
bashfor i in $(seq 20); do bun test 2>&1 | tail -1; done
Si sur 20 runs tu as 18 passes et 2 échecs, tu tiens ton flaky.
Pour un suivi plus structure, GitLab CI a une fonctionnalité de détection de flaky tests dans les rapports JUnit. Les tests qui echouent puis passent sans changement de code sont automatiquement marques.
La quarantaine
Quand tu identifies un flaky test et que tu ne peux pas le corriger immédiatement, mets-le en quarantaine :
typescriptit.skip("flaky: sends notification (timing issue)", async () => {
// TODO: fix polling logic, see issue #234
// ...
});
C'est mieux que de le laisser pourrir le pipeline. Mais ne l'oublie pas. Un test en quarantaine qui reste 6 mois c'est un test mort. Mets une issue, assigne-la, fixe une deadline.
La regle d'or
Un test qui echoue doit signifier "il y a un bug dans le code". Pas "le serveur externe etait lent" ou "les tests ont tourne dans le mauvais ordre". Si ton équipe regle un pipeline rouge en cliquant "retry", ton pipeline a un problème de confiance. Et c'est un problème que seule la correction des flaky tests peut résoudre.
Article précédent : 02 - Coverage : le piège du 100%
Article suivant : 04 - Tests de regression