DonnIA
Toutes les ressources
GuideBonnes pratiques14 min

Faire écrire des tests qui servent à quelque chose

Un test qui n'a jamais échoué ne prouve rien : la méthode pour vérifier qu'il détecte le bug avant de le corriger.


« Corrige ce bug et ajoute un test » produit presque toujours la même séquence : l'agent modifie le code, lance la suite, tout passe, et il ajoute un test qui passe lui aussi. Le résultat a l'air impeccable. Il ne prouve rien.

Le test a été écrit après la correction, contre le code déjà corrigé. Il décrit ce que le code fait maintenant, pas ce qu'il devrait faire. Si tu annules la correction, il y a une chance sérieuse que le test continue de passer, parce qu'il a été calibré sur le comportement observé et non sur l'attendu. C'est le mode de défaillance principal des tests écrits par un agent, et il est invisible dans une sortie verte.

Il existe un contrôle unique qui l'élimine : un test n'a de valeur que si tu l'as vu échouer pour la bonne raison. Tout ce guide tourne autour de cette phrase.

Le test qui n'a jamais été rouge

Un test qui n'a jamais échoué est une hypothèse, pas une preuve. Il peut être tautologique, viser la mauvaise fonction, avoir un assert qui ne s'exécute jamais parce qu'il est après un return, ou passer par un mock si complet que c'est le mock qu'on teste.

Aucun de ces défauts ne se voit dans un rapport vert. Tous se voient immédiatement si tu exiges un passage par le rouge.

La suite verte n'est pas un signal

Un agent optimise ce que tu observes. Si tu observes « la suite passe », tu obtiendras une suite qui passe, au besoin en affaiblissant les assertions. Le signal utile, c'est la transition rouge puis vert, avec les deux sorties sous les yeux.

Avant de commencer

  • Un projet avec un lanceur de tests déjà configuré
  • Un bug réel, ou un comportement que tu veux figer
  • De quoi annuler une modification (git stash)

1Décrire le bug sans le corriger

Sépare les deux temps. Tant que la reproduction n'est pas stable, tout test écrit est une devinette.

Ne corrige rien pour l'instant. Reproduis ce bug et decris-le en trois
lignes : l'entree exacte, la sortie observee, la sortie attendue.
Si tu n'arrives pas a le reproduire, dis-le au lieu de proposer une
correction plausible.

La dernière phrase compte. Sans elle, un agent qui ne reproduit pas le bug part quand même corriger quelque chose, et tu te retrouves avec un test qui protège un chemin qui n'a jamais posé de problème.

2Écrire le test, et le regarder échouer

Le test s'écrit contre le code cassé, pas contre le code corrigé.

Ecris un seul test qui echoue a cause de ce bug. Le code reste inchange.
Lance uniquement ce test et montre-moi la sortie complete, y compris le
message d'assertion.
Je veux voir l'echec avant qu'on parle de correction.

Pour n'exécuter que ce test, la syntaxe d'identifiant de test est plus fiable qu'un filtre par mot-clé :

pytest tests/test_facture.py::test_remise_negative

-x arrête la suite à la première défaillance, --lf rejoue uniquement les tests tombés au tour précédent, et -k "expression" filtre par nom quand tu ne connais pas l'identifiant exact.

Lis le message d'échec, pas seulement sa présence. Une ImportError ou une TypeError est un échec, mais ce n'est pas l'échec attendu : le test ne démontre alors rien du bug.

Le bon rouge

L'échec doit venir de l'assertion métier, avec les deux valeurs affichées : ce qu'on attendait, ce qu'on a obtenu. Si l'échec vient d'un import, d'une fixture manquante ou d'une erreur de syntaxe, le test n'est pas encore prêt.

3Corriger sans toucher au test

Le test devient la référence. Il ne bouge plus.

Corrige maintenant le code pour que ce test passe. Interdit de modifier
le fichier de test, meme d'une ligne. Si tu penses que le test est faux,
arrete-toi et explique pourquoi au lieu de l'ajuster.

Sans cette contrainte, la trajectoire habituelle est : le test résiste, l'agent assouplit l'assertion, la suite repasse au vert. Le bug est toujours là, et il est désormais couvert par un test qui le valide.

4Vérifier que le test détecte, et pas seulement qu'il décrit

Un test peut passer avant et après la correction sans que personne le remarque. Le contrôle est mécanique :

git stash
pytest tests/test_facture.py::test_remise_negative
git stash pop

Si le test passe avec la correction annulée, il ne teste pas le bug. Jette-le et recommence à l'étape 2.

Le même principe s'étend aux tests existants, en cassant volontairement le code plutôt qu'en le restaurant :

Prends la fonction calculer_remise. Introduis une erreur realiste, par
exemple inverser une comparaison ou decaler une borne.
Dis-moi quels tests tombent. Si aucun ne tombe, la fonction n'est pas
couverte : dis-le clairement et ne modifie rien d'autre.

C'est l'idée derrière les outils de test par mutation, appliquée à la main sur les fonctions qui comptent. La couverture de lignes te dit qu'un code a été exécuté, jamais qu'il a été vérifié.

5Interdire les tests décoratifs

Certains tests coûtent du temps d'exécution et ne protègent de rien. Mieux vaut les nommer d'avance dans la consigne.

Regles pour tous les tests que tu ecris :
- une assertion sur une valeur precise, pas sur un type ni sur "pas
  d'exception" ;
- pas de mock de la fonction qu'on teste, ni de sa valeur de retour ;
- pas d'instantane genere automatiquement pour un comportement qu'on
  n'a jamais lu ;
- un nom qui decrit la regle metier, pas la fonction appelee ;
- si un cas est impossible a tester proprement, dis-le au lieu
  d'ecrire un test vide.

Le nom du test est un signal sous-estimé. test_calculer_remise ne dit rien. test_remise_negative_est_refusee décrit une règle, et devient faux le jour où la règle change, ce qui est exactement le rôle d'un test.

Ce que cette méthode ne couvre pas

Le passage par le rouge valide un test contre un bug connu. Il ne dit rien de deux autres questions.

Les tests qui manquent. Un agent n'écrit que les tests que tu demandes, et la couverture de lignes ne mesure pas ce qui est vérifié : elle mesure ce qui a été exécuté. Un fichier peut afficher 90 % de couverture sans qu'une seule assertion porte sur une valeur de retour. La question utile n'est pas « quel est le taux » mais « qu'est-ce qui doit casser pour que cette suite devienne rouge ».

Le coût de la suite. Chaque test ajouté est du temps d'exécution à chaque changement, et une maintenance à chaque refonte. Un agent produit volontiers quinze tests là où trois suffisent, parce que rien dans la demande ne l'incite à s'arrêter. Fixer un plafond dans la consigne, par exemple trois tests par correction, force le choix des cas qui comptent.

Ces deux points relèvent d'une décision humaine sur ce qui mérite d'être protégé. Ce qui s'automatise, c'est la vérification qu'un test donné détecte bien quelque chose. C'est déjà l'essentiel de ce qui manque aux suites écrites par un agent.

Quelques prompts pour aller plus loin

ObjectifPrompt à adapter
Auditer une suite existantePour chaque test de ce fichier, dis ce qui doit casser dans le code pour qu'il echoue. Signale ceux pour lesquels tu ne trouves aucune reponse.
Trouver les trous de couvertureListe les branches de ce module qu'aucun test n'exerce. Classe-les par consequence si elles cassent en production.
Figer une régressionOn vient de corriger [BUG]. Ecris le test qui aurait du l'attraper, verifie qu'il echoue sur le commit precedent, et montre les deux sorties.
Réduire une suite lenteTrouve les tests qui verifient la meme regle plusieurs fois. Propose lesquels supprimer, en justifiant que rien n'est perdu.
Tester un cas limiteListe les valeurs limites de cette fonction : vide, zero, negatif, tres grand, mauvais type, doublon. Dis lesquelles sont deja testees.

Tu as d’autres ressources comme ça ?

Ce que tu viens de lire est entièrement gratuit. Une nouvelle ressource part chaque semaine dans la newsletter : guides, modèles à copier et liens triés, sans compte à créer.

Cette page est en accès libre, n’hésite pas à la partager.