Résumé
Un programme qui compile et écrit quelque chose de raisonnable n’est pas, pour autant, testé. Cette leçon enseigne à tester avec méthode : à dériver les cas de la spécification, par régions et par frontières ; à les réunir dans un programme de tests automatisés qui compte les échecs et les signale ; à vérifier avec assert ce qui ne peut pas arriver si le programme est correct, et avec static_assert, à la compilation, ce qu’une fonction constexpr doit calculer ; et à exécuter les tests avec les détecteurs, qui découvrent le comportement indéfini qu’un résultat correct cache. Tout repose sur un avertissement : un test peut montrer la présence d’une erreur, jamais son absence.
Objectifs d’apprentissage
- Dériver les cas de test de la spécification d’une fonction : les partitions de son entrée, les frontières entre elles et les cas particuliers, avec le résultat que la postcondition exige dans chacun.
- Écrire un programme de tests automatisés qui compare les résultats obtenus aux résultats attendus, signale chaque échec sans s’arrêter et se termine par un résumé et un code de sortie qui le reflète.
- Utiliser
assertpour vérifier des préconditions et des invariants, et expliquer pourquoi il disparaît quand on compile avec-DNDEBUGet pourquoi sa condition ne doit pas avoir d’effets de bord. - Utiliser
static_assertavec des fonctionsconstexprpour tester des résultats à la compilation, et exécuter les tests avec les détecteurs pour découvrir le comportement indéfini que les résultats ne révèlent pas.
Ce qu’est tester
Tester un programme, c’est l’exécuter avec des entrées choisies et comparer ce qu’il fait à ce qu’il devait faire. Chaque cas de test se compose de deux parties : une entrée qui satisfait la précondition, et le résultat que la postcondition exige pour elle, calculé à l’avance et par un autre chemin (à la main, avec une autre formule, avec une table connue). Sans le résultat attendu, il n’y a pas de test, mais une exécution : regarder la sortie et la trouver « raisonnable » est une impression, non une comparaison.
Or une fonction d’un argument int admet plus de quatre milliards d’entrées, et une fonction de deux arguments, environ paires ; aucune batterie de tests ne les parcourt toutes. S’il en est ainsi, les tests examinent une part infime des cas possibles, et du fait qu’ils passent tous il ne s’ensuit pas que la fonction soit correcte. Edsger Dijkstra l’a formulé en 1970 : les tests peuvent montrer la présence d’erreurs, jamais leur absence. La distinction est celle que le cours Logique mathématique I établit entre vérifier des cas et démontrer : vérifier qu’une formule est vraie pour quelques valeurs ne prouve pas qu’elle le soit pour toutes, alors qu’une démonstration (dans ce cours, l’invariant de la leçon Concevoir des boucles correctes) couvre tous les cas à la fois. Les tests ne remplacent pas ce raisonnement : ils trouvent les erreurs qui lui ont échappé.
De là découle le critère pour choisir les cas. Si les tests ne peuvent pas confirmer la correction, leur valeur est de trouver des erreurs, et un bon cas de test est celui qui a le plus de chances d’échouer. Un cas que l’auteur a choisi parce qu’il savait qu’il fonctionnerait n’apprend rien. Le programme suivant le montre avec une fonction qui doit renvoyer le plus grand élément d’un vecteur non vide.
#include <print>#include <vector>// Plus grand élément d'une liste de données.// Précondition : values n'est pas vide.// Postcondition : renvoie le plus grand élément de values.int largest(const std::vector<int>& values){ int best{0}; for (const int v : values) { if (v > best) { best = v; } } return best;}int main(){ std::println("{}", largest({3, 9, 4})); std::println("{}", largest({7})); std::println("{}", largest({1, 2, 3, 4})); std::println("{}", largest({-5, -2, -8}));}9740Les trois premiers cas sont ceux que n’importe qui écrit d’abord, et la fonction les passe : 9, 7 et 4 sont corrects. Le quatrième donne 0, une valeur qui n’est même pas dans le vecteur, alors qu’il devait donner . L’erreur est à la ligne 9 : best commence à 0 et non au premier élément, de sorte que la fonction n’est correcte que si un élément au moins est positif ou nul. Les cas typiques partageaient cette propriété sans que personne l’ait choisie ; le quatrième la viole exprès. La correction est int best{values[0]};, que la précondition rend légitime, car elle garantit que values[0] existe.
Chargement du contenu…