04 - Tests de regression : ne jamais recasser ce qui marchait
Ce que tu vas apprendre
- Le workflow bug -> test rouge -> fix -> test vert
- Pourquoi un bug sans test reviendra forcement
- Utiliser
git bisectpour trouver le commit coupable - Un vrai exemple de regression sur paltemps.fr
Prerequisites
Savoir écrire des tests unitaires et d'intégration. Connaitre les bases de git.
Le bug qui revient
Mars 2025. Un utilisateur de paltemps.fr signale que les dates de création des missions s'affichent en format americain (03/28/2025 au lieu de 28/03/2025). Je corrige. Trois mois plus tard, juin 2025, un autre utilisateur signale le meme bug. Meme fonction, meme problème. Un collegue avait refactorise le module de formatage et reintroduit le bug sans le savoir.
Ca m'a pris 15 minutes a diagnostiquer la première fois, 20 minutes la deuxieme (parce que je ne me souvenais plus du contexte), et 0 minute la troisieme fois -- parce que cette fois j'ai écrit un test.
Le workflow de regression
Le principe est simple. Quand un bug est reporte, avant de le corriger :
- Ecris un test qui reproduit le bug. Ce test doit échouer (rouge).
- Corrige le code.
- Le test passe (vert).
- Ce bug ne reviendra plus jamais.
L'étape 1 est la plus importante. Si tu corriges d'abord et que tu ecris le test apres, tu ne sais jamais vraiment si ton test aurait détecté le bug. En ecrivant le test d'abord, tu prouves qu'il le détecté.
typescript// Etape 1 : le test qui echoue (reproduit le bug)
it("formats date in French format dd/mm/yyyy", () => {
const date = new Date("2025-03-28T12:00:00Z");
expect(formatDate(date)).toBe("28/03/2025");
// FAIL: recoit "03/28/2025"
});
typescript// Etape 2 : le fix
export function formatDate(date: Date): string {
// Avant : date.toLocaleDateString("en-US")
return date.toLocaleDateString("fr-FR");
}
typescript// Etape 3 : le test passe maintenant
// Et si quelqu'un retouche cette fonction dans 6 mois, le test echouera
Pourquoi ca marche
Un test de regression est une preuve. Il prouve trois choses :
Premierement, que le bug existait. Le test echoue avant le fix, donc on sait qu'il reproduit le problème.
Deuxiemement, que le fix corrige le bug. Le test passe apres le fix.
Troisiemement, que le bug ne revient pas. Le test fait partie de la suite. Il tourne a chaque push dans la CI (voir l'article 00). Si quelqu'un reintroduit le bug, le pipeline echoue.
Sans ce test, tu depends de la mémoire humaine. "Ah oui, il ne faut pas toucher a cette fonction parce qu'il y avait un bug de format..." Ca ne tient pas. Les gens oublient, les équipes changent, le code evolue.
git bisect : trouver le commit coupable
Parfois tu decouvres une regression mais tu ne sais pas quand elle est apparue. Le code marchait il y a deux semaines, maintenant c'est casse. Il y a eu 47 commits entre temps. Lequel a casse ?
git bisect fait une recherche dichotomique dans l'historique :
bashgit bisect start
git bisect bad # le commit actuel est casse
git bisect good abc123 # ce vieux commit marchait
# Git checkout un commit au milieu
# Tu testes :
bun test --filter "formatDate"
# Si ca passe :
git bisect good
# Si ca echoue :
git bisect bad
# Git continue a couper en deux jusqu'a trouver LE commit
# En general 6-7 etapes pour 100 commits
A la fin, git te dit exactement quel commit a introduit la regression. Tu peux meme automatiser :
bashgit bisect start HEAD abc123
git bisect run bun test --filter "formatDate"
Git lance le test a chaque étape et identifié le commit coupable tout seul. Sur paltemps.fr, j'ai utilise ca pour retrouver un commit qui avait change un >= en > dans une condition de tri. 3 minutes avec bisect contre potentiellement une heure a lire des diffs.
Un vrai cas de regression
Voici un cas concret. Sur paltemps.fr, on a un endpoint qui retourne les missions disponibles filtrees par date. Un jour, les missions du jour meme ne s'affichent plus. Bug report.
D'abord le test de regression :
typescriptit("includes missions starting today", async () => {
// Creer une mission qui commence aujourd'hui
const today = new Date();
today.setHours(0, 0, 0, 0);
await db.insert(missions).values({
title: "Mission test",
startDate: today,
endDate: new Date(today.getTime() + 86400000),
});
const res = await app.handle(
new Request("http://localhost/api/missions?date=" + today.toISOString().split("T")[0])
);
const data = await res.json();
// Ce test echoue : la mission du jour n'est pas retournee
expect(data.missions).toHaveLength(1);
expect(data.missions[0].title).toBe("Mission test");
});
Le coupable ? Une requête SQL avec startDate > :date au lieu de startDate >= :date. Le > strict excluait les missions qui commencaient exactement a minuit le jour meme.
Le fix est une lettre. Mais sans le test, ce bug serait revenu. Parce qu'un jour quelqu'un aurait "optimise" la requête, ou l'aurait reecrite, et le meme piège se serait referme.
Combien de tests de regression accumuler ?
Sur paltemps.fr, on en a une trentaine apres un an. Chacun porte un commentaire qui référencé le ticket ou le bug report :
typescript// Regression: #142 - Missions du jour exclues du filtre
it("includes missions starting today", async () => {
// ...
});
Le commentaire avec le numero de ticket, c'est pas du decoratif. Dans 6 mois, quand quelqu'un se demande pourquoi ce test existe, il peut remonter au ticket et comprendre le contexte.
La regle
Si tu ne retiens qu'une chose de cet article : chaque bug corrige sans test est un bug qui reviendra. Peut-etre pas demain, peut-etre pas le mois prochain. Mais un jour, quelqu'un touchera a ce code, et le meme problème reapparaitra. Le test de regression, c'est ton filet de sécurité permanent.
Article précédent : 03 - Flaky tests
Article suivant : 05 - TDD vs BDD vs test-after