Chapitre 2 : Spécification et tests¶
Programme officiel (B.O.)¶
B.O. spécial n° 1 du 22 janvier 2019 - NSI Première
| Contenus | Capacités attendues | Commentaires |
|---|---|---|
| Spécification. | Prototyper une fonction. Décrire les préconditions sur les arguments. Décrire des postconditions sur les résultats. | Des assertions peuvent être utilisées pour vérifier des préconditions. |
| Jeux de tests. | Utiliser des jeux de tests. | L'élève est exposé aux pratiques de tests unitaires et peut utiliser un framework de tests. |
1. Spécifier une fonction¶
1.1 Pourquoi spécifier ?¶
Écrire un programme juste est plus important qu'écrire un programme qui « fonctionne ». La spécification d'une fonction décrit précisément ce qu'elle fait, avant même de l'implémenter.
Une bonne spécification répond à trois questions :
- Quoi ? — Que fait la fonction ? (description)
- Avec quoi ? — Quels sont les paramètres et leurs types ? (préconditions)
- Pour quoi ? — Que renvoie-t-elle ? (postconditions)
1.2 Le prototype¶
Le prototype d'une fonction décrit son nom, ses paramètres et leur type, ainsi que le type de la valeur renvoyée.
En Python, les annotations de type (: list, -> float) sont facultatives mais très utiles pour documenter le code.
1.3 La docstring¶
La docstring est une chaîne de caractères placée juste après la ligne def, qui décrit le comportement de la fonction :
def moyenne(notes: list) -> float:
"""Calcule la moyenne d'une liste de nombres.
Preconditions:
notes est une liste non vide de nombres (int ou float).
Postconditions:
Renvoie la moyenne arithmétique des éléments de notes.
"""
return sum(notes) / len(notes)
1.4 Préconditions et postconditions¶
| Terme | Définition | Exemple |
|---|---|---|
| Précondition | Condition que les arguments doivent vérifier pour que la fonction fonctionne correctement | notes est non vide, contient des nombres |
| Postcondition | Propriété que le résultat doit vérifier si les préconditions sont respectées | Le résultat est compris entre min(notes) et max(notes) |
Les préconditions et postconditions forment un contrat entre l'utilisateur de la fonction et son auteur :
- L'utilisateur s'engage à respecter les préconditions
- L'auteur garantit que les postconditions seront satisfaites
1.5 Exemple complet de spécification¶
def recherche(tableau: list, valeur) -> int:
"""Recherche la première occurrence de valeur dans tableau.
Preconditions:
tableau est une liste (éventuellement vide).
valeur est du même type que les éléments de tableau.
Postconditions:
Renvoie l'indice de la première occurrence de valeur dans tableau.
Renvoie -1 si valeur n'est pas dans tableau.
"""
for i in range(len(tableau)):
if tableau[i] == valeur:
return i
return -1
2. Les assertions¶
2.1 Qu'est-ce qu'une assertion ?¶
Une assertion est une instruction qui vérifie qu'une condition est vraie. Si la condition est fausse, le programme s'arrête immédiatement avec une erreur AssertionError.
On peut ajouter un message d'erreur :
2.2 Vérifier les préconditions avec assert¶
Les assertions sont utiles pour vérifier les préconditions au début d'une fonction :
def moyenne(notes: list) -> float:
"""Calcule la moyenne d'une liste de nombres."""
assert isinstance(notes, list), "notes doit être une liste"
assert len(notes) > 0, "La liste ne doit pas être vide"
assert all(isinstance(n, (int, float)) for n in notes), "Tous les éléments doivent être des nombres"
return sum(notes) / len(notes)
>>> moyenne([12, 15, 8, 14])
12.25
>>> moyenne([])
AssertionError: La liste ne doit pas être vide
>>> moyenne("texte")
AssertionError: notes doit être une liste
2.3 Vérifier les postconditions¶
On peut aussi vérifier les postconditions :
def division_entiere(a: int, b: int) -> tuple:
"""Renvoie (quotient, reste) de la division de a par b.
Preconditions: a >= 0, b > 0, a et b entiers.
Postconditions: a == quotient * b + reste et 0 <= reste < b.
"""
assert isinstance(a, int) and isinstance(b, int), "a et b doivent être entiers"
assert a >= 0, "a doit être positif ou nul"
assert b > 0, "b doit être strictement positif"
q = a // b
r = a % b
# Vérification de la postcondition
assert a == q * b + r, "Postcondition violée"
assert 0 <= r < b, "Le reste doit être entre 0 et b-1"
return (q, r)
3. Les jeux de tests¶
3.1 Pourquoi tester ?¶
Comme le disait Edsger Dijkstra : « Tester un programme démontre la présence de bugs, pas leur absence. »
Malgré cette limite, les tests sont indispensables pour :
- Vérifier que la fonction produit les résultats attendus
- Détecter les régressions lors de modifications
- Documenter le comportement attendu
- Réfléchir aux cas limites
3.2 Écrire des tests avec assert¶
La méthode la plus simple consiste à écrire des assertions qui comparent le résultat de la fonction avec le résultat attendu :
def valeur_absolue(x):
"""Renvoie la valeur absolue de x."""
if x >= 0:
return x
else:
return -x
# Jeu de tests
assert valeur_absolue(5) == 5, "Nombre positif"
assert valeur_absolue(-3) == 3, "Nombre négatif"
assert valeur_absolue(0) == 0, "Zéro"
assert valeur_absolue(-0.5) == 0.5, "Flottant négatif"
print("Tous les tests passent !")
3.3 Choisir les cas de test¶
Un bon jeu de tests doit couvrir :
| Type de cas | Description | Exemple pour valeur_absolue |
|---|---|---|
| Cas normal | Entrée courante | valeur_absolue(5) → 5 |
| Cas limite | Valeur aux frontières | valeur_absolue(0) → 0 |
| Cas extrême | Grandes ou petites valeurs | valeur_absolue(-1000000) → 1000000 |
| Cas d'erreur | Entrée invalide | valeur_absolue("texte") → erreur |
3.4 Exemple : tester une fonction de tri¶
def tri_selection(tableau):
"""Trie un tableau en place par sélection."""
for i in range(len(tableau)):
i_min = i
for j in range(i + 1, len(tableau)):
if tableau[j] < tableau[i_min]:
i_min = j
tableau[i], tableau[i_min] = tableau[i_min], tableau[i]
# Tests
t1 = [3, 1, 4, 1, 5]
tri_selection(t1)
assert t1 == [1, 1, 3, 4, 5], "Cas normal"
t2 = []
tri_selection(t2)
assert t2 == [], "Tableau vide"
t3 = [42]
tri_selection(t3)
assert t3 == [42], "Un seul élément"
t4 = [1, 2, 3, 4, 5]
tri_selection(t4)
assert t4 == [1, 2, 3, 4, 5], "Déjà trié"
t5 = [5, 4, 3, 2, 1]
tri_selection(t5)
assert t5 == [1, 2, 3, 4, 5], "Trié à l'envers"
t6 = [7, 7, 7, 7]
tri_selection(t6)
assert t6 == [7, 7, 7, 7], "Éléments identiques"
print("Tous les tests passent !")
3.5 Le module doctest¶
Le module doctest permet d'inclure les tests directement dans la docstring de la fonction, sous forme d'exemples d'utilisation :
def factorielle(n):
"""Calcule n! (factorielle de n).
Preconditions: n est un entier >= 0.
>>> factorielle(0)
1
>>> factorielle(1)
1
>>> factorielle(5)
120
>>> factorielle(10)
3628800
"""
if n == 0:
return 1
resultat = 1
for i in range(1, n + 1):
resultat = resultat * i
return resultat
if __name__ == "__main__":
import doctest
doctest.testmod()
Si tous les tests passent, rien ne s'affiche. Avec l'option -v (verbose), on obtient le détail :
4. Développement piloté par les tests (TDD)¶
Le TDD (Test Driven Development) est une méthode de développement qui consiste à :
- Écrire les tests d'abord (ils échouent car la fonction n'existe pas encore)
- Écrire le code minimal pour que les tests passent
- Améliorer le code si nécessaire (les tests garantissent qu'il reste correct)
# Étape 1 : écrire les tests
assert est_palindrome("kayak") == True
assert est_palindrome("python") == False
assert est_palindrome("") == True
assert est_palindrome("a") == True
assert est_palindrome("aba") == True
assert est_palindrome("ab") == False
# Étape 2 : écrire la fonction
def est_palindrome(mot):
"""Vérifie si mot est un palindrome."""
return mot == mot[::-1]
# Étape 3 : tous les tests passent
À retenir¶
- Spécifier une fonction, c'est décrire ce qu'elle fait, ses préconditions (contraintes sur les entrées) et ses postconditions (garanties sur le résultat).
- Le prototype donne le nom, les paramètres, leurs types et le type de retour.
- La docstring décrit le comportement de la fonction sous la ligne
def. - Les assertions (
assert condition) vérifient les préconditions et les postconditions. En cas d'échec, le programme s'arrête. - Un jeu de tests vérifie le bon fonctionnement de la fonction avec des cas normaux, limites, extrêmes et d'erreur.
- Le module
doctestpermet d'intégrer les tests dans la docstring. - Le TDD consiste à écrire les tests avant le code.