Chapitre 1115 minIntermédiaire
Tests de non-régression : définition, méthode et exemples
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- définir une régression et un test de non-régression ;
- corriger un bug en écrivant d'abord le test qui le reproduit ;
- choisir quels tests rejouer, et à quel moment ;
- tenir une liste de non-régression pour la recette ;
- retrouver le commit fautif avec git bisect.
4 exercices corrigés
Une régression est un défaut qui apparaît dans une fonctionnalité qui marchait, à la suite d'une modification du logiciel : une correction, une évolution, une mise à jour de bibliothèque. Le test de non-régression consiste à rejouer, après chaque modification, les tests qui passaient avant, pour vérifier que rien n'a été cassé. On dit aussi test de régression : les deux expressions désignent la même chose, l'une insiste sur la recherche des régressions, l'autre sur leur absence. Ce chapitre montre comment construire une suite de non-régression, corriger un bug sans en créer d'autres, et retrouver le commit qui a introduit une régression.
Pourquoi les régressions arrivent
Un programme est un réseau de dépendances. Modifier un endroit peut avoir des effets ailleurs, que personne ne voit en écrivant la modification.
| Cause | Exemple dans Prêt'Matos |
|---|---|
| Une modification d'une fonction partagée | Yanis change periodesSeChevauchent pour la demande de Karim ; la fonction sert aussi à la vérification des réservations, qui accepte désormais deux prêts le même jour |
| Une correction de bug | Pour corriger l'affichage des dates dans l'export CSV, Lina change formaterDate ; les motifs de refus de l'API changent de format, et le front, qui les affiche, montre des dates à l'américaine |
| Une évolution | L'ajout des congés dans niveauDeRelance oublie la valeur par défaut : toutes les relances lèvent une erreur |
| Une mise à jour de dépendance | Une nouvelle version majeure d'une bibliothèque de dates change le résultat d'une fonction de formatage |
| Un changement de configuration ou d'environnement | Le serveur passe en fuseau horaire UTC : la « date du jour » change à 1 heure du matin au lieu de minuit |
Le point commun : la personne qui fait la modification ne pense pas à vérifier ce qu'elle ne savait pas avoir touché. C'est pour cela que la non-régression doit être systématique et automatisée, et non laissée à l'intuition.
La suite de non-régression
Une suite de non-régression n'est pas un type de test à part : c'est l'ensemble des tests existants, que l'on rejoue après chaque modification. Elle grossit de deux façons :
- Avec le développement : chaque fonctionnalité arrive avec ses tests unitaires, d'intégration et parfois de bout en bout ;
- Avec les bugs : chaque défaut corrigé arrive avec le test qui l'aurait détecté. Il porte souvent le numéro du ticket dans son nom.
Plus le projet vit, plus la suite protège. Une application de trois ans, dont chaque bug corrigé a laissé un test, ne peut presque plus régresser sur ses anciens défauts.
Corriger un bug avec un test de non-régression
Reprenons l'histoire du chapitre 1. Le ticket n° 31 arrive : « PC-07 a été réservé par Julie du 10 au 14 mars et par Sofiane du 14 au 18 mars. » La méthode tient en cinq étapes.
1. Reproduire le bug dans un test, et le voir échouer. Avant de toucher au code, on écrit le test qui décrit le comportement attendu.
// src/regles.test.js, dans le describe de periodesSeChevauchent
test('ticket 31 : une période qui commence le dernier jour chevauche', () => {
const pc07 = { debut: '2027-03-10', fin: '2027-03-14' };
expect(periodesSeChevauchent(pc07, { debut: '2027-03-14', fin: '2027-03-18' })).toBe(true);
});
Lancé sur le code de Yanis (avec < au lieu de <= dans la seconde comparaison), il échoue : expected false to be true. Le bug est reproduit : on sait exactement ce qui ne va pas, et on saura quand c'est corrigé.
2. Corriger la cause. On remet numeroDeJour(b.debut) <= numeroDeJour(a.fin). Et on règle le vrai besoin de Karim autrement, avec la logistique : une demi-journée de battement n'est pas une affaire de comparaison de dates, c'est une nouvelle règle à spécifier.
3. Voir le test passer. Le test du ticket 31 est vert.
4. Rejouer toute la suite. npm test : tous les tests passent, y compris ceux des autres fonctions. C'est le test de non-régression au sens strict : la correction n'a rien cassé ailleurs.
5. Livrer avec la trace. Le commit cite le ticket (Corrige le chevauchement des périodes contiguës (ticket 31)), le test reste dans la suite pour toujours, et le ticket est clos avec le lien vers le correctif.
Le cours Maintenance applicative détaille cette démarche, avec la branche de correctif, la recherche de la cause et la livraison (corriger un bug sans régression).
Quoi rejouer, et quand
Rejouer tous les tests à chaque modification est l'idéal, tant que c'est rapide. Quand la suite grossit, on organise les rejeux selon leur coût :
| Moment | Ce qu'on rejoue | Qui le lance |
|---|---|---|
| À chaque enregistrement de fichier | Les tests unitaires concernés (vitest en mode surveillance relance ceux qui dépendent du fichier modifié) | Le développeur, automatiquement |
| Avant chaque commit | Toute la suite unitaire et d'intégration | Le développeur, ou un hook Git |
| À chaque push et chaque pull request | Unitaires, intégration, analyse statique | La CI |
Avant la fusion sur main, ou chaque nuit | Les tests de bout en bout, sur plusieurs navigateurs | La CI |
| Avant chaque mise en production | La campagne de non-régression de la recette : tests automatisés et scénarios manuels critiques | L'équipe et la MOA, sur l'environnement de recette |
Quand on ne peut pas tout rejouer (une campagne manuelle, par exemple), on sélectionne les tests par risque : d'abord les fonctions touchées par la modification et celles qui en dépendent, puis les fonctions critiques pour le métier (réserver, enregistrer une sortie), puis le reste si le temps le permet.
La campagne de non-régression en recette
Une partie des vérifications reste manuelle : l'ergonomie, les impressions, les envois de courriels réels sur l'environnement de recette. Pour ne rien oublier d'une version à l'autre, on tient une liste de non-régression : les scénarios critiques rejoués avant chaque mise en production, quelle que soit la modification.
| N° | Scénario critique | Résultat attendu | v1.4 | v1.5 |
|---|---|---|---|---|
| NR-01 | Se connecter avec le compte d'un salarié | Le catalogue s'affiche | OK | OK |
| NR-02 | Réserver un matériel libre | Réservation visible dans « mes réservations » | OK | OK |
| NR-03 | Réserver un matériel déjà réservé sur une partie de la période | Refus avec les dates de la réservation existante | OK | KO (A-12) |
| NR-04 | Salarié : ouvrir l'écran « enregistrer une sortie » par son adresse | Accès refusé (403) | OK | OK |
| NR-05 | Logistique : enregistrer la sortie puis le retour | Statuts mis à jour, historique complet | OK | OK |
| NR-06 | Recevoir le rappel la veille du retour (boîte de test) | Un courriel, le bon matériel, la bonne date | OK | OK |
La colonne de chaque version garde la mémoire des campagnes ; un KO renvoie vers une fiche d'anomalie. Les scénarios qui reviennent à chaque version et ne demandent pas de jugement humain sont les premiers à automatiser : NR-03 et NR-04 sont déjà couverts par les tests d'intégration du chapitre 8. Le chapitre 14 montre comment rédiger ces cas et le rapport de la campagne.
Les tests d'instantané et la non-régression visuelle
Un test d'instantané (snapshot test) enregistre la sortie d'une fonction ou d'un composant lors du premier lancement, puis vérifie aux lancements suivants qu'elle n'a pas changé. C'est une forme de non-régression : il ne dit pas si la sortie est juste, seulement si elle est identique.
test("le motif de refus garde son format", () => {
const resultat = verifierReservation(
{ materiel: 'PC-07', debut: '2027-03-12', fin: '2027-03-18' },
[{ materiel: 'PC-07', debut: '2027-03-10', fin: '2027-03-14' }],
'2027-03-08',
);
expect(resultat).toMatchInlineSnapshot();
});
Au premier lancement, Vitest écrit la valeur obtenue dans le fichier de test, à l'intérieur des parenthèses :
expect(resultat).toMatchInlineSnapshot(`
{
"acceptee": false,
"motif": "PC-07 est déjà réservé du 10/03/2027 au 14/03/2027.",
}
`);
Si Lina change plus tard formaterDate, le test échoue et montre la différence. Si le changement est voulu, on met à jour les instantanés avec npx vitest run -u. toMatchSnapshot(), sans « Inline », range les instantanés dans un dossier __snapshots__ à côté du fichier de test.
Playwright propose la même idée pour l'apparence : await expect(page).toHaveScreenshot() compare une capture de la page à une capture de référence, au pixel près (avec une tolérance réglable). C'est la non-régression visuelle : elle détecte un bouton qui disparaît ou une mise en page cassée par une modification CSS.
Les mises à jour de dépendances
Les bibliothèques d'un projet évoluent, et les mettre à jour corrige des failles de sécurité. Mais chaque mise à jour peut introduire une régression. La suite de non-régression rend ces mises à jour sûres : un outil comme Dependabot ou Renovate ouvre une pull request pour chaque nouvelle version, la CI rejoue la suite, et l'équipe fusionne si tout est vert. Sans tests, on n'ose plus mettre à jour, et l'application accumule des dépendances vulnérables. Le cours Maintenance applicative traite ce sujet (versions et dépendances).
Retrouver le commit fautif avec git bisect
Une régression est signalée, mais personne ne sait quand elle est apparue : la version 1.2 marchait, la version actuelle non, et cent commits les séparent. git bisect fait une recherche dichotomique dans l'historique : il te place au milieu, tu dis si le défaut est présent, il coupe en deux la moitié restante, et ainsi de suite. Pour cent commits, sept étapes suffisent (2⁷ = 128).
Avec un test qui reproduit le défaut, la recherche est entièrement automatique :
git bisect start
git bisect bad # le commit actuel a le défaut
git bisect good v1.2.0 # cette version ne l'avait pas
git bisect run npx vitest run src/ticket31.test.js
git bisect reset # revenir à la branche de départ
git bisect run lance la commande à chaque étape : un code de retour 0 (tests verts) signifie « bon », un autre code (tests rouges) signifie « mauvais ». À la fin, Git affiche le premier commit fautif, son auteur et son message. Ici, src/ticket31.test.js contient le test du ticket 31 et n'est pas commité pendant la recherche. Le test doit exister dans toutes les versions testées : c'est pour cela qu'on l'écrit dans un fichier non suivi par Git, que les changements de commit ne touchent pas.
Exercices
Exercice 1 sur 4 Écrire le test de non-régression d'un ticket Réussi À revoir
Ticket n° 23 : « Un salarié a pu réserver VP-02 du 25 février au 11 mars 2028. La durée dépasse 15 jours. » En lisant le code, Lina trouve la cause dans une ancienne version de dureeEnJours :
export function dureeEnJours(debut, fin) {
return new Date(fin).getDate() - new Date(debut).getDate() + 1;
}
- Combien de jours dure la réservation du ticket ? Que calcule la fonction ?
- Écris le test de non-régression du ticket, sur
verifierReservation. - Quel second test, accepté cette fois, ajouterais-tu pour encadrer la limite ?
Voir la réponseMasquer la réponse de l'exercice 1
- 2028 est bissextile : du 25 au 29 février, 5 jours, puis du 1er au 11 mars, 11 jours, soit 16 jours. La fonction ne regarde que le numéro du jour dans le mois : 11 − 25 + 1 = −13. Comme −13 n'est pas supérieur à 15, la réservation est acceptée. Toute réservation à cheval sur deux mois est mal calculée.
test('ticket 23 : refuse un prêt de 16 jours à cheval sur deux mois', () => {
const demande = { materiel: 'VP-02', debut: '2028-02-25', fin: '2028-03-11' };
expect(verifierReservation(demande, [], '2028-02-01').motif).toBe('Un prêt dure au plus 15 jours.');
});
- Un prêt de 15 jours à cheval sur deux mois, qui doit être accepté : du 26 février au 11 mars 2028 (4 + 11 = 15 jours).
test('accepte un prêt de 15 jours à cheval sur deux mois', () => {
const demande = { materiel: 'VP-02', debut: '2028-02-26', fin: '2028-03-11' };
expect(verifierReservation(demande, [], '2028-02-01').acceptee).toBe(true);
});
Avec la version correcte de dureeEnJours (celle du chapitre 4), les deux tests passent ; avec l'ancienne, le premier échoue.
Tu avais trouvé ?
Exercice 2 sur 4 Quels tests rejouer ? Réussi À revoir
Une correction est livrée ce soir en production : le format des dates dans l'export CSV. Il reste une heure pour la campagne de non-régression manuelle en recette, qui en compte six (NR-01 à NR-06 ci-dessus). La suite automatisée est verte. Lesquels rejoues-tu en priorité, et pourquoi ?
Voir la réponseMasquer la réponse de l'exercice 2
La suite automatisée couvre déjà les règles et l'API. En manuel, on choisit par risque :
- L'export CSV lui-même (la fonction modifiée), avec un fichier ouvert dans le tableur de la logistique.
- NR-03 : le motif de refus affiche des dates. Si la correction a touché
formaterDate, partagée avec l'export, le format des dates à l'écran a pu changer. - NR-06 : le courriel de rappel contient aussi une date formatée.
- Puis NR-02 et NR-05, les parcours critiques du métier, si le temps le permet.
NR-01 et NR-04 ne dépendent pas des dates ; ils sont couverts par les tests automatisés et peuvent attendre la campagne complète de la prochaine version.
Tu avais trouvé ?
Exercice 3 sur 4 L'instantané validé trop vite Réussi À revoir
Un développeur modifie formaterDate pour l'export CSV. Trois tests d'instantané échouent. Il lance npx vitest run -u, tout passe au vert, il pousse. Le lendemain, les salariés voient « PC-07 est déjà réservé du 2027-03-10 au 2027-03-14 ». Qu'aurait-il dû faire ?
Voir la réponseMasquer la réponse de l'exercice 3
Lire les différences affichées par les trois tests avant de mettre à jour quoi que ce soit. Elles montraient que le motif de refus changeait de format, un effet de bord de sa modification sur une fonction partagée. Deux corrections possibles : créer une fonction de formatage propre à l'export CSV, en laissant formaterDate intacte ; ou, si le changement de format était voulu partout, le faire valider et mettre à jour les instantanés en connaissance de cause. Un test d'instantané signale un changement ; c'est à toi de dire s'il est voulu. Une assertion précise (toBe('PC-07 est déjà réservé du 10/03/2027 au 14/03/2027.')) aurait d'ailleurs été plus explicite qu'un instantané.
Tu avais trouvé ?
Exercice 4 sur 4 Combien d'étapes avec git bisect ? Réussi À revoir
Entre la version qui marchait et la version actuelle, il y a 300 commits. Combien d'étapes de git bisect, au plus, faut-il pour trouver le commit fautif ? Et pour 1 000 commits ?
Voir la réponseMasquer la réponse de l'exercice 4
Chaque étape divise par deux le nombre de commits suspects. Il faut le plus petit n tel que 2ⁿ ≥ nombre de commits : pour 300 commits, 2⁸ = 256 ne suffit pas, 2⁹ = 512 suffit, donc 9 étapes au plus. Pour 1 000 commits, 2¹⁰ = 1 024 : 10 étapes. C'est la même logique que la recherche dichotomique dans un tableau trié (algorithmique).
Tu avais trouvé ?