Aller au contenu principal
outilsTests, style et automatisation

Tests, style et automatisation

Ce que ce chapitre apporte

  • Dire à quoi sert un test automatisé, et ce qu'il ne prouve pas.
  • Structurer un test en trois temps et le nommer pour qu'il se lise seul.
  • Choisir ce qui mérite un test et ce qui n'en mérite pas.
  • Expliquer le fonctionnement d'un lanceur de tests.
  • Interpréter un taux de couverture sans en faire un objectif.
  • Distinguer formateur, linteur et analyseur de types.
  • Automatiser ces vérifications au bon moment.
Où on va
Un test n'existe pas pour prouver qu'un programme est juste, ce qu'aucun test ne peut faire. Il existe pour qu'on puisse modifier le programme sans avoir peur. C'est une différence de nature : sans tests, chaque changement dans un code qu'on ne connaît pas par cœur devient un pari. Ce chapitre traite de ce qu'on écrit comme test, de ce qu'on n'écrit pas, et des vérifications qu'on peut confier à la machine pour ne plus jamais en discuter.

À quoi sert un test

Trois usages, dans l'ordre où on les rencontre.

Vérifier maintenant. Le test remplace l'essai manuel : au lieu de relancer le programme et de regarder l'écran, on écrit une fois la vérification et la machine la refait.

Décrire. Un test bien nommé dit ce que la fonction est censée faire, y compris aux cas limites. C'est souvent la seule documentation qui reste exacte, puisqu'un test faux échoue.

Protéger. C'est le point décisif. Une suite de tests permet de restructurer du code, de mettre à jour une dépendance, d'accepter la contribution d'un inconnu, sans passer une journée à vérifier à la main que rien n'a cassé.

Ce qu'un test ne prouve pas
Qu'un programme est correct. Tester montre la présence de défauts, jamais leur absence : une suite qui passe indique seulement que les cas prévus se comportent comme prévu.
La question utile n'est donc pas « combien de tests », mais « quels cas ». Un seul test bien choisi sur une frontière vaut mieux que trente sur des variantes du même cas nominal.

L'anatomie d'un test

Trois temps, toujours les mêmes : préparer les données, appeler ce qu'on teste, vérifier le résultat.

def test_moyenne_ignore_les_notes_absentes():
    notes = [12, None, 16]          # préparer

    resultat = moyenne(notes)       # agir

    assert resultat == 14           # vérifier

Le nom est la partie la plus utile. test_moyenne n'apprend rien quand il échoue au milieu de la nuit. test_moyenne_ignore_les_notes_absentes dit immédiatement quel comportement a disparu.

Un test vérifie un comportement. Un test qui contient huit vérifications s'arrête à la première qui échoue, et masque les sept autres.

Ce qu'un lanceur de tests fait vraiment

Un outil comme pytest paraît magique parce qu'on ne lui déclare rien : il trouve les tests seul, les exécute, et rapporte. Le mécanisme tient en une vingtaine de lignes.

main.py
Sortie
>_ Prêt à exécuter…

Trois symboles dans ce rapport, et la distinction compte. Le point signale un test qui passe. Le F signale une assertion fausse : le code s'exécute, mais ne renvoie pas ce que le test décrit ; il faut alors décider qui a tort, du code ou de l'attente. Le E signale une erreur inattendue, ici une division par zéro sur une liste vide : le code n'a même pas prévu ce cas, et le trou est plus profond.

Écrire le test d'abord, au moins pour les corrections
Quand un défaut est signalé, écrire d'abord le test qui le reproduit a deux vertus : il prouve que le défaut est compris, et il échoue, ce qui garantit qu'il teste réellement quelque chose.
Un test écrit après la correction et qui passe du premier coup ne prouve rien : il aurait aussi bien pu passer avant.

Que tester

Par ordre de rentabilité :

Les frontières. Zéro élément, un élément, la valeur maximale, la limite exacte d'une condition. C'est là que se logent la majorité des défauts.

Les règles métier. Un calcul de remise, un arrondi, une règle de gestion. Ce sont elles qui coûtent cher quand elles sont fausses, et elles changent souvent.

Les régressions. Chaque défaut corrigé laisse un test derrière lui. C'est la seule façon de ne pas le voir revenir.

Les chemins d'erreur. Fichier absent, réseau coupé, entrée mal formée. Rarement testés, souvent faux, parce que personne ne les exerce à la main.

Ce qui ne mérite pas de test : les accesseurs triviaux, le code d'une bibliothèque qu'on n'a pas écrite, et l'affichage exact d'un message qui changera demain.

La couverture

Définition

La couverture de code est la proportion de lignes, ou de branches, exécutées au moins une fois pendant la suite de tests.

C'est un bon détecteur et un mauvais objectif. Une ligne jamais exécutée n'est certainement pas testée : le rapport de couverture montre donc les zones oubliées, souvent les traitements d'erreur.

En revanche, une ligne exécutée n'est pas une ligne vérifiée. Un test qui appelle une fonction sans rien contrôler du résultat fait monter la couverture sans rien garantir. Fixer un seuil de 90 % à une équipe produit exactement ces tests-là.

Formateur, linteur, analyseur de types

Trois outils souvent confondus, qui ne répondent pas à la même question.

OutilQuestionExemples
Formateurcomment le code est-il disposé ?Black, Prettier
Linteury a-t-il des constructions douteuses ?Ruff, ESLint
Analyseur de typesles types sont-ils cohérents ?mypy, TypeScript

Le formateur ne discute pas : il réécrit, et il n'y a rien à en dire. Le linteur signale une variable inutilisée, un import mort, une comparaison suspecte, une exception attrapée trop largement. L'analyseur de types attrape ce qu'aucun des deux ne voit, par exemple une fonction qui peut renvoyer None et dont l'appelant ne s'en préoccupe pas.

Terminal4 commandes
$black .# met en forme, sans rien demander
$ruff check .# signale les constructions douteuses
$mypy src/# vérifie la cohérence des types
$pytest# exécute la suite de tests
Le test qui échoue une fois sur dix
Un test instable est pire qu'un test absent. Il apprend à l'équipe à relancer sans lire, et le jour où il échoue pour une vraie raison, personne ne le voit.
Les causes habituelles sont une dépendance à l'heure, à un ordre d'exécution, à un service extérieur, ou à un état laissé par un autre test. Un test instable se répare ou se supprime, il ne se tolère pas.

Automatiser au bon moment

Chaque vérification a un moment naturel.

À l'enregistrement, dans l'éditeur : le formatage. Instantané, jamais discuté.

Avant le commit, par un hook : le formatage et le linteur sur les fichiers modifiés. Quelques secondes au plus, sinon les hooks seront contournés.

À la poussée ou dans la demande de fusion : toute la suite de tests, l'analyse de types, la construction. C'est le rôle de l'intégration continue, objet du chapitre suivant.

# .git/hooks/pre-commit, rendu exécutable par chmod +x
#!/bin/sh
ruff check . || exit 1
black --check . || exit 1
Un hook local ne remplace pas une vérification centrale
Les hooks vivent dans .git/hooks, qui ne se versionne pas, et n'importe qui peut les contourner avec --no-verify. Ils rendent service à l'auteur, ils ne garantissent rien à l'équipe.
La garantie vient de la vérification exécutée sur le serveur, que personne ne peut sauter.

Exercices type

Pourquoi un test qui passe du premier coup, écrit après la correction, ne prouve-t-il rien ?

Parce qu'on n'a pas montré qu'il échoue quand le défaut est présent.

Un test peut passer pour de mauvaises raisons : il n'exerce pas le chemin concerné, son assertion est trop faible, ou il vérifie autre chose que ce qu'on croit.

La séquence qui donne une garantie est : écrire le test, le voir échouer, corriger, le voir passer. C'est aussi ce qui permet d'affirmer que le défaut a été compris.

Une suite de tests passe à 100 %. Que peut-on en conclure ?

Que les cas prévus se comportent comme prévu. Rien de plus.

Cela ne dit rien des cas auxquels personne n'a pensé, qui sont précisément ceux où se trouvent les défauts restants.

Tester montre la présence de défauts, pas leur absence. La question utile est de savoir quels cas sont couverts, en particulier les frontières et les chemins d'erreur, pas de compter les tests.

Pourquoi une couverture de 90 % peut-elle être un mauvais objectif ?

Parce que la couverture mesure les lignes exécutées, pas les comportements vérifiés. Un test qui appelle une fonction sans rien contrôler fait monter le chiffre sans apporter de garantie.

Imposer un seuil crée une incitation à écrire ces tests-là, les moins chers, qui donnent une fausse confiance.

L'usage correct du rapport est de le lire pour repérer les zones jamais exécutées, souvent les traitements d'erreur, et d'y écrire de vrais tests.

Quelle différence entre un formateur et un linteur ?

Le formateur agit sur la disposition du code : indentation, longueur des lignes, position des retours à la ligne. Il réécrit le fichier, et son résultat ne se discute pas.

Le linteur agit sur le contenu : variable inutilisée, import mort, condition toujours vraie, exception attrapée trop largement. Il signale, et parfois propose une correction.

Le premier supprime un sujet de désaccord, le second attrape des défauts réels. Ils sont complémentaires, pas concurrents.

Un test échoue une fois sur dix. Que faire ?

Le traiter comme un défaut prioritaire, pas comme une gêne.

Chercher la source d'indétermination : dépendance à l'horloge, à un ordre d'exécution, à un état partagé entre tests, à un service extérieur, ou à une exécution concurrente.

En attendant la correction, le désactiver explicitement plutôt que de le laisser échouer par intermittence. Un test rouge par moments finit par être ignoré, et avec lui tous les autres.

Pourquoi ne pas mettre toute la suite de tests dans un hook avant commit ?

Parce que la durée décide de l'adoption. Un hook de deux secondes est respecté ; un hook de trois minutes est contourné par --no-verify dès le deuxième jour.

Parce que les hooks ne se versionnent pas et ne s'appliquent qu'à ceux qui les ont installés : ils ne garantissent rien à l'échelle de l'équipe.

Le partage naturel est : le rapide et le local avant le commit, le long et l'exhaustif sur le serveur, où personne ne peut le sauter.

La méthode

  1. Écris le test avant la correction, et vérifie qu'il échoue.
  2. Un comportement par test, avec un nom qui dit lequel.
  3. Commence par les frontières et les chemins d'erreur, pas par le cas nominal.
  4. Laisse un test derrière chaque défaut corrigé.
  5. Lis la couverture pour trouver les trous, n'en fais pas un objectif.
  6. Confie le style à un outil, et cesse d'en parler.
  7. Répare ou supprime un test instable, ne le tolère jamais.

En résumé

  • Un test sert d'abord à pouvoir changer le code sans crainte.
  • Il montre la présence de défauts, jamais leur absence.
  • Trois temps : préparer, agir, vérifier. Un comportement par test.
  • Un lanceur collecte les fonctions de test, les exécute, et distingue échec d'assertion et erreur.
  • Les frontières, les règles métier, les régressions et les chemins d'erreur d'abord.
  • La couverture est un détecteur de trous, pas un objectif.
  • Formateur, linteur et analyseur de types répondent à trois questions différentes.
  • Rapide et local avant le commit, complet et central sur le serveur.
  • Un test instable se répare ou se supprime.

Et ensuite ? Ces vérifications n'ont de valeur que si elles s'exécutent toujours, chez tout le monde, sans qu'on ait à y penser. C'est le rôle de l'intégration continue.

Tests, style et automatisation | Plateforme ETS