15 - Spécification et tests

Exercice 1 : QCM – vérification des prérequis

Pour chaque question, une seule réponse est correcte.

1. À quoi sert une docstring dans une fonction Python ?

  • A. À accélérer l’exécution de la fonction
  • B. À documenter ce que fait la fonction, ses paramètres et sa valeur de retour
  • C. À empêcher la modification de la fonction
  • D. À afficher automatiquement le résultat de la fonction
Correction

Réponse : B. La docstring est une chaîne de caractères placée au début du corps de la fonction (entre """..."""). Elle décrit le rôle de la fonction, ses paramètres (types, contraintes) et ce qu’elle renvoie. On y accède avec help(ma_fonction) ou ma_fonction.__doc__.

  • A est faux : la docstring n’a aucun effet sur les performances.
  • C est faux : rien n’empêche de modifier une fonction en Python.
  • D est faux : la docstring ne produit aucun affichage lors de l’appel.

2. Que fait l’instruction assert condition, message en Python ?

  • A. Elle affiche le message
  • B. Si la condition est vraie, elle lève une erreur avec le message
  • C. Si la condition est fausse, elle lève une erreur AssertionError avec le message
  • D. Elle commente le code pour la documentation
Correction

Réponse : C. assert vérifie une condition. Si elle est fausse, Python lève une AssertionError et affiche le message. Si elle est vraie, rien ne se passe et l’exécution continue.

  • A est faux : assert ne sert pas à afficher, c’est le rôle de print.
  • B est faux : c’est l’inverse (l’erreur est levée quand la condition est fausse).
  • D est faux : assert est une instruction exécutée, pas un commentaire.

3. Qu’appelle-t-on une précondition ?

  • A. Le résultat attendu de la fonction
  • B. Une condition qui doit être vraie avant l’appel de la fonction pour qu’elle fonctionne correctement
  • C. Une variable globale utilisée par la fonction
  • D. La première ligne de code de la fonction
Correction

Réponse : B. Une précondition est une hypothèse sur les données d’entrée. Par exemple, pour une fonction racine(x), la précondition est $x \geq 0$. Si la précondition n’est pas respectée, le comportement de la fonction n’est pas garanti.

  • A est faux : le résultat attendu s’appelle la postcondition.
  • C est faux : les variables globales n’ont rien à voir avec les préconditions.
  • D est faux : la première ligne de code est le début de l’implémentation, pas la spécification.

4. Quel est l’intérêt d’écrire des tests pour une fonction ?

  • A. Accélérer l’exécution de la fonction
  • B. Vérifier automatiquement que la fonction produit les résultats attendus sur des cas connus
  • C. Empêcher les utilisateurs de modifier la fonction
  • D. Supprimer les bugs sans avoir à lire le code
Correction

Réponse : B. Les tests vérifient que la fonction se comporte comme prévu sur des cas choisis. Ils permettent de détecter les erreurs (régressions) lorsqu’on modifie le code, et de documenter le comportement attendu par l’exemple.

  • A est faux : les tests n’ont aucun effet sur la vitesse.
  • C est faux : les tests ne protègent pas le code contre la modification.
  • D est faux : les tests détectent les bugs mais ne les corrigent pas automatiquement.

Exercice 2 : exemple travaillé – spécifier et tester une fonction

Problème : écrire une fonction qui vérifie si un nombre est premier.

Étape 1. Spécification (avant d’écrire le code) :

def est_premier(n):
    """Détermine si un entier est premier.

    Paramètres :
        n (int) : entier à tester

    Précondition :
        n >= 0

    Retour :
        bool : True si n est premier, False sinon
    """

Étape 2. Tests (avant d’écrire l’implémentation) :

# Cas de base
assert est_premier(0) == False   # 0 n'est pas premier
assert est_premier(1) == False   # 1 n'est pas premier
assert est_premier(2) == True    # 2 est premier (le seul pair)

# Cas classiques
assert est_premier(7) == True
assert est_premier(9) == False   # 9 = 3 × 3

# Grand nombre
assert est_premier(97) == True
assert est_premier(100) == False

Étape 3. Implémentation :

def est_premier(n):
    """Détermine si un entier est premier.

    Paramètres :
        n (int) : entier à tester

    Précondition :
        n >= 0

    Retour :
        bool : True si n est premier, False sinon
    """
    assert n >= 0, "n doit être un entier positif"
    if n < 2:
        return False
    for d in range(2, n):
        if n % d == 0:
            return False
    return True

Points clés :

  • La spécification (docstring) est écrite avant le code : elle définit le contrat de la fonction.
  • Les tests sont écrits avant l’implémentation : c’est le principe du développement guidé par les tests.
  • L’assert au début vérifie la précondition : si n n’est pas un entier positif, l’erreur est détectée immédiatement.

Exercice 3 : spécifications guidées

3.1 Compléter une docstring

Compléter la docstring de la fonction suivante :

def moyenne(L):
    """___

    Paramètres :
        L (___) : ___

    Précondition :
        ___

    Retour :
        ___ : ___
    """
    total = 0
    for x in L:
        total += x
    return total / len(L)
Correction
def moyenne(L):
    """Calcule la moyenne arithmétique d'une liste de nombres.

    Paramètres :
        L (list) : liste de nombres (int ou float)

    Précondition :
        L est non vide (len(L) > 0)

    Retour :
        float : la moyenne des éléments de L
    """
    total = 0
    for x in L:
        total += x
    return total / len(L)

La précondition len(L) > 0 est essentielle : si L est vide, la division par len(L) provoque une erreur ZeroDivisionError.

3.2 Écrire des tests

Écrire au moins cinq assertions pour tester la fonction moyenne :

Correction
# Liste d'un seul élément
assert moyenne([5]) == 5.0

# Entiers
assert moyenne([10, 20]) == 15.0

# Flottants
assert abs(moyenne([1.5, 2.5, 3.0]) - 2.333) < 0.01

# Nombres négatifs
assert moyenne([-4, 4]) == 0.0

# Valeurs identiques
assert moyenne([7, 7, 7]) == 7.0

print("Tous les tests passent.")

Choix des tests :

  • Un seul élément (cas limite).
  • Deux éléments (cas simple).
  • Flottants (vérification avec tolérance à cause de l’arithmétique flottante).
  • Nombres négatifs (cas piégeux : la moyenne peut être nulle).
  • Valeurs identiques (vérification triviale).

Exercice 4 : préconditions et assert (vie courante)

1. Écrire une fonction indice_masse_corporelle(masse, taille) qui calcule l’IMC. Ajouter des assert pour vérifier les préconditions : la masse doit être strictement positive et la taille doit être strictement positive et inférieure à trois mètres.

2. Écrire une fonction note_sur_20(points, total) qui convertit un nombre de points en note sur 20. Ajouter des assert pour vérifier que points >= 0, total > 0 et points <= total.

3. Tester chaque fonction avec des valeurs valides et des valeurs qui violent les préconditions (observer les erreurs).

Correction
def indice_masse_corporelle(masse, taille):
    """Calcule l'indice de masse corporelle.

    Préconditions :
        masse > 0 (en kg)
        0 < taille < 3 (en mètres)
    """
    assert masse > 0, "La masse doit être strictement positive"
    assert 0 < taille < 3, "La taille doit être entre 0 et 3 mètres"
    return round(masse / taille ** 2, 1)

def note_sur_20(points, total):
    """Convertit un nombre de points en note sur 20.

    Préconditions :
        points >= 0
        total > 0
        points <= total
    """
    assert points >= 0, "Les points ne peuvent pas être négatifs"
    assert total > 0, "Le total doit être strictement positif"
    assert points <= total, "Les points ne peuvent pas dépasser le total"
    return round(points / total * 20, 1)

# Tests valides
assert indice_masse_corporelle(70, 1.75) == 22.9
assert note_sur_20(15, 25) == 12.0
assert note_sur_20(0, 20) == 0.0
assert note_sur_20(20, 20) == 20.0

# Tests de violation des préconditions (à tester un par un)
# indice_masse_corporelle(-5, 1.75)   # AssertionError
# indice_masse_corporelle(70, 0)      # AssertionError
# note_sur_20(25, 20)                 # AssertionError

print("Tests OK")

Les assert protègent la fonction contre des entrées incohérentes. En cas de violation, le message d’erreur aide à identifier le problème.


Exercice 5 : jeux de tests – couverture des cas

Pour chaque fonction ci-dessous, proposer un jeu de tests couvrant les cas suivants : cas normal, cas limite, cas d’erreur.

1. est_pair(n) qui renvoie True si n est pair, False sinon.

2. maximum(a, b, c) qui renvoie le plus grand des trois nombres.

3. chercher(L, x) qui renvoie l’indice de x dans L, ou -1 si x n’est pas dans L.

Correction
def est_pair(n):
    """Renvoie True si n est pair."""
    return n % 2 == 0

# Cas normal
assert est_pair(4) == True
assert est_pair(7) == False
# Cas limite
assert est_pair(0) == True    # 0 est pair
# Cas négatif
assert est_pair(-6) == True
assert est_pair(-3) == False

def maximum(a, b, c):
    """Renvoie le plus grand des trois nombres."""
    if a >= b and a >= c:
        return a
    elif b >= c:
        return b
    else:
        return c

# Cas normal (max en 1re, 2e, 3e position)
assert maximum(10, 5, 3) == 10
assert maximum(3, 10, 5) == 10
assert maximum(3, 5, 10) == 10
# Cas avec égalités
assert maximum(5, 5, 5) == 5
assert maximum(7, 7, 3) == 7
# Cas négatifs
assert maximum(-1, -5, -2) == -1

def chercher(L, x):
    """Renvoie l'indice de x dans L, ou -1."""
    for i in range(len(L)):
        if L[i] == x:
            return i
    return -1

# Cas normal
assert chercher([3, 1, 4, 1, 5], 4) == 2
# Élément au début / à la fin
assert chercher([3, 1, 4], 3) == 0
assert chercher([3, 1, 4], 4) == 2
# Élément absent
assert chercher([3, 1, 4], 7) == -1
# Liste vide
assert chercher([], 5) == -1
# Doublons (premier trouvé)
assert chercher([1, 2, 1], 1) == 0

print("Tous les tests passent.")

Méthodologie de test :

  • Cas normal : des valeurs typiques pour vérifier le fonctionnement de base.
  • Cas limite : valeurs extrêmes (0, liste vide, élément en première ou dernière position).
  • Cas piégeux : valeurs négatives, doublons, égalités.

Exercice 6 : déboguer grâce aux tests (informatique)

Chacune des fonctions suivantes contient une erreur. Utiliser les tests fournis pour identifier et corriger le bug.

1.

def valeur_absolue(n):
    """Renvoie la valeur absolue de n."""
    if n > 0:
        return n
    return n

# Tests :
assert valeur_absolue(5) == 5
assert valeur_absolue(0) == 0
assert valeur_absolue(-3) == 3   # Ce test échoue !

2.

def est_dans(L, x):
    """Renvoie True si x est dans L."""
    for element in L:
        if element == x:
            return True
        else:
            return False

# Tests :
assert est_dans([1, 2, 3], 1) == True
assert est_dans([1, 2, 3], 3) == True   # Ce test échoue !
assert est_dans([], 5) == False

3.

def compter(L, x):
    """Compte le nombre d'occurrences de x dans L."""
    compteur = 0
    for element in L:
        if element == x:
            compteur += 1
        return compteur

# Tests :
assert compter([1, 2, 1, 3, 1], 1) == 3   # Ce test échoue !
assert compter([1, 2, 3], 4) == 0
Correction

1. Le bug est dans la seconde branche : pour un nombre négatif, la fonction renvoie n au lieu de -n. Pour $n = -3$, la condition n > 0 est fausse et la fonction renvoie -3 : le test échoue, ce qui révèle le bug. La correction :

def valeur_absolue(n):
    if n >= 0:
        return n
    else:
        return -n

2. Le bug est l’indentation du return False : il est dans le else à l’intérieur de la boucle. Dès le premier élément, si ce n’est pas x, la fonction renvoie False sans vérifier les éléments suivants. Le return False doit être après la boucle :

def est_dans(L, x):
    for element in L:
        if element == x:
            return True
    return False

3. Le bug est l’indentation du return compteur : il est à l’intérieur de la boucle for. La fonction renvoie le compteur dès la première itération au lieu de parcourir toute la liste. Le return doit être après la boucle :

def compter(L, x):
    compteur = 0
    for element in L:
        if element == x:
            compteur += 1
    return compteur

Leçon : les erreurs d’indentation (surtout avec return dans une boucle) sont parmi les bugs les plus fréquents en Python. Les tests automatiques permettent de les détecter rapidement.


Exercice 7 : synthèse – spécifier, coder et tester un convertisseur

On souhaite écrire un convertisseur de températures complet.

1. Écrire la spécification (docstring avec paramètres, préconditions et retour) d’une fonction celsius_vers_fahrenheit(c).

Rappel : $F = \frac{9}{5} \times C + 32$

2. Écrire un jeu de tests avant l’implémentation, couvrant :

  • le point de congélation de l’eau (0 °C → 32 °F) ;
  • le point d’ébullition (100 °C → 212 °F) ;
  • une température négative (−40 °C → −40 °F, cas remarquable où les deux échelles se croisent) ;
  • le zéro absolu (−273,15 °C → −459,67 °F).

3. Implémenter la fonction avec une vérification de la précondition (la température ne peut pas être inférieure au zéro absolu).

4. Écrire la fonction inverse fahrenheit_vers_celsius(f) avec sa spécification, ses tests et sa vérification de précondition.

5. Vérifier la cohérence des deux fonctions : pour toute température $c$, on doit avoir fahrenheit_vers_celsius(celsius_vers_fahrenheit(c))c (à une erreur d’arrondi près).

Correction
def celsius_vers_fahrenheit(c):
    """Convertit une température de Celsius en Fahrenheit.

    Paramètres :
        c (float) : température en degrés Celsius

    Précondition :
        c >= -273.15 (zéro absolu)

    Retour :
        float : température en degrés Fahrenheit
    """
    assert c >= -273.15, "Température inférieure au zéro absolu"
    return round(9 / 5 * c + 32, 2)

def fahrenheit_vers_celsius(f):
    """Convertit une température de Fahrenheit en Celsius.

    Paramètres :
        f (float) : température en degrés Fahrenheit

    Précondition :
        f >= -459.67 (zéro absolu en Fahrenheit)

    Retour :
        float : température en degrés Celsius
    """
    assert f >= -459.67, "Température inférieure au zéro absolu"
    return round((f - 32) * 5 / 9, 2)

# Tests celsius_vers_fahrenheit
assert celsius_vers_fahrenheit(0) == 32
assert celsius_vers_fahrenheit(100) == 212
assert celsius_vers_fahrenheit(-40) == -40
assert celsius_vers_fahrenheit(-273.15) == -459.67

# Tests fahrenheit_vers_celsius
assert fahrenheit_vers_celsius(32) == 0
assert fahrenheit_vers_celsius(212) == 100
assert fahrenheit_vers_celsius(-40) == -40
assert fahrenheit_vers_celsius(-459.67) == -273.15

# Test de cohérence (aller-retour)
for c in [-273.15, -40, 0, 37, 100]:
    resultat = fahrenheit_vers_celsius(celsius_vers_fahrenheit(c))
    assert abs(resultat - c) < 0.01, f"Erreur pour c = {c}"

print("Tous les tests passent.")

Bilan : une fonction bien spécifiée et bien testée est plus fiable, plus facile à maintenir et plus facile à déboguer. La méthodologie consiste à écrire dans l’ordre : la spécification (docstring), les tests (assert), puis l’implémentation.