Spécification et tests
Rappel d'ouverture (5 minutes, cours fermé)
Réponds sans rouvrir les pages précédentes, en écrivant tes réponses.
- Écris la signature typée d'une fonction
est_majeurqui prend un âge entier et renvoie un booléen. - Que renvoie une fonction qui n'a aucune instruction
return? - Combien de tours fait
for i in range(0, 10, 3)?
Corrigé
def est_majeur(age: int) -> bool:- Elle renvoie
None. Une fonction renvoie toujours quelque chose ; sansreturn, c'estNone. D'où le piègereturncontreprint. - Quatre tours :
iprend0,3,6,9. La borne10est exclue.
Écrire une fonction, c'est une chose. S'assurer qu'elle fait vraiment ce qu'on attend en est une autre. C'est le sujet de cette page, et c'est sans doute la partie la plus importante du travail d'un programmeur.
1. Spécifier : dire ce que fait la fonction
Spécifier une fonction, c'est décrire ce qu'elle prend en entrée et ce qu'elle rend en sortie, sans dire comment. C'est le rôle de la signature et de la docstring, vues dans Les fonctions.
La signature dit : « je prends un flottant, je renvoie un flottant ». La docstring dit : « ce flottant, c'est le carré de l'entrée ». On sait exactement ce que la fonction doit faire, avant même de l'écrire.
Toujours dans cet ordre
- La signature (le contrat) ;
- la docstring (ce que fait la fonction) ;
- le code ;
- les tests, qui vérifient que le code respecte le contrat.
2. Tester automatiquement : assert
Python offre un moyen simple de vérifier qu'une fonction renvoie la bonne valeur : l'instruction assert.
- Si la condition est vraie, il ne se passe rien : le test passe.
- Si elle est fausse, Python lève une erreur (
AssertionError) avec le message, et arrête le programme.
Corriger grâce à un test
Voici une fonction carre volontairement fausse. Le test le détecte. Corrige le code, puis relance : plus aucune erreur ne doit apparaître.
Ce qu'un test change dans la façon de chercher un bug
Sans test, une erreur logique ne se signale pas : le programme tourne et donne un résultat faux. Un assert transforme cette erreur silencieuse en erreur bruyante, qui s'arrête et nomme la ligne. Autrement dit, il fait passer le bug le plus coûteux dans la catégorie la moins coûteuse.
D'où la discipline, la même que sur la boucle infinie : une hypothèse précise avant chaque modification. Devant un assert qui casse, on n'édite pas le code au hasard. On dit d'abord « je pense que le résultat est trop grand de 1 parce que la boucle va jusqu'à n inclus », puis on vérifie cette phrase-là. Si elle est fausse, on en formule une autre.
Un bon test couvre plusieurs cas
Un seul assert ne suffit pas. Pense aux cas particuliers : 0, un nombre négatif, une liste vide, le plus petit ou le plus grand cas possible. Un bug se cache souvent dans un cas qu'on n'a pas testé.
3. Des exemples qui deviennent des tests : les doctests
On peut placer les exemples directement dans la docstring, sous la forme >>> suivie du résultat attendu. Ces exemples documentent la fonction et servent de tests.
def carre(x: float) -> float:
"""Renvoie le carré du flottant x.
>>> carre(3)
9
>>> carre(-4)
16
"""
return x * x
Chaque ligne >>> carre(3) indique un appel, et la ligne suivante le résultat attendu. Python peut vérifier automatiquement tous ces exemples d'un coup. C'est le format utilisé dans la plupart des exercices de ce cours.
4. Préconditions et postconditions
Le contrat d'une fonction a deux faces :
- une précondition : ce qui doit être vrai avant l'appel, sur les arguments ;
- une postcondition : ce que la fonction garantit sur son résultat si la précondition est respectée.
On vérifie souvent une précondition avec un assert au début de la fonction :
def minimum(lst: list[int]) -> int:
"""Renvoie le plus petit élément de lst.
Précondition : lst n'est pas vide.
>>> minimum([7, 2, 9, 2, 5])
2
"""
assert len(lst) > 0, "la liste ne doit pas être vide"
...
Ici, la précondition « la liste n'est pas vide » est nécessaire : le minimum d'une liste vide n'a pas de sens. L'assert protège la fonction contre un usage incorrect.
Une liste, déjà ?
Les listes ne sont étudiées que plus loin, dans Les listes. Elles servent ici uniquement d'exemple de donnée sur laquelle une précondition a un sens évident. Retiens le contrat, pas la manipulation : len(lst) > 0 se lit « la liste contient au moins un élément ».
5. Pourquoi tu dois savoir le faire toi-même
Spécifier et tester à l'ère de l'IA
Soyons exacts, parce que tu peux le vérifier en trente secondes : une IA générative écrit une fonction carre en une seconde, et elle sait aussi écrire sa spécification, ses préconditions et ses tests. Prétendre qu'elle en est incapable serait faux.
La question n'est donc pas ce que la machine sait faire, mais ce que tu sais faire.
- Juger. Face à une réponse toute faite, tu n'as que deux positions possibles : la croire, ou la vérifier. Vérifier suppose de savoir dire ce que le code devait faire, cas limites compris. Ce n'est pas la machine qui te donne cette capacité, c'est l'entraînement que tu fais ici.
- Décider. Une spécification comporte des choix (que faire d'une liste vide ? d'un nombre négatif ?). Une machine en propose un, plausible. Savoir lequel te convient suppose de savoir à quoi servira ta fonction.
- Répondre de ce que tu rends. Un devoir, un projet, plus tard un programme utilisé par d'autres : la responsabilité reste celle de la personne qui signe, pas de l'outil.
- Être évalué sans elle. Les évaluations de NSI, en première comme à l'épreuve pratique de terminale, se passent sans IA.
Une étude récente sur des débutants qui programment avec une IA (Prather et al., 2024) observe précisément cela : l'outil aide beaucoup ceux qui savent déjà juger ce qu'ils lisent, et creuse l'écart pour les autres. Savoir spécifier et tester, c'est ce qui te met du bon côté de cet écart.
6. Exercices
Spécifier, tester, puis coder
Pour chaque fonction, écris d'abord la signature et la docstring, puis au moins deux assert, et seulement ensuite le code.
aire_rectangle(largeur, hauteur): l'aire d'un rectangle.cube(n): le cube d'un entier.est_pair(n):Truesinest pair.celsius_vers_fahrenheit(t): convertit une température (\(°F = °C \times 1{,}8 + 32\)).
Corrigé (exemple pour les deux premières)
def aire_rectangle(largeur: float, hauteur: float) -> float:
"""Renvoie l'aire d'un rectangle largeur x hauteur."""
return largeur * hauteur
assert aire_rectangle(14, 50) == 700
assert aire_rectangle(0, 5) == 0
def cube(n: int) -> int:
"""Renvoie le cube de l'entier n."""
return n ** 3
assert cube(2) == 8
assert cube(-3) == -27
assert cube(0) == 0