4. Tester le code produit#

Dans le cadre de la production de code, il est souvent nécessaire de fournir des interfaces documentées, et de vérifier, sur un certain nombre d’exemples que les fonctions et modules produits correspondent bien aux attentes du donneur d’ordre.

Pour cela, on utilise des cadriciels [1] de tests. Certains, comme doctest sont intégrés à Python (au sens qu’il s’agit d’une bibliothèque distribuée avec Python), d’autres nécessitent l’installation de modules tiers[2].

Contenus

Capacités attendues

Commentaires

Mise au point des programmes

Utiliser des jeux de tests

L’importance de la qualité et du nombre de tests est mise en évidence.

Le succès d’un jeu de tests ne garantit pas la correction d’un programme.

4.1. Doctest#

Il s’agit de l’infrastructure minimaliste de test, livrée dans la distribution standard de Python. Cette infrastructure est rudimentaire, mais est essentielle, surtout pour son aspect de documentation.

Voyons ça sur un exemple :

def addition(a: int, b: int) -> int:
    """
    Une fonction de démonstration de doctest

    Cette fonction n'a pour but que de montrer le fonctionnement de doctest.

    Args:
        a (int): le premier terme de la somme
        b (int): le deuxième terme de la somme
    Returns:
        int: le résultat de la somme

    Exemple:
    >>> addition(1,2)
    3
    >>> addition(1,2.2)
    Traceback (most recent call last):
    Exception: Les arguments doivent être des entiers, ici on a reçu a=1 et b=2.2
    """
    if not (isinstance(a,int) and isinstance(b, int)):
        raise Exception(f'Les arguments doivent être des entiers, ici on a reçu a={a} et b={b}')
    return a + b

Testons « à la main » la fonction.

addition(1,2)
3
addition(1,2.2)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 addition(1,2.2)

Cell In[1], line 21, in addition(a, b)
      2 """
      3 Une fonction de démonstration de doctest
      4 
   (...)
     18 Exception: Les arguments doivent être des entiers, ici on a reçu a=1 et b=2.2
     19 """
     20 if not (isinstance(a,int) and isinstance(b, int)):
---> 21     raise Exception(f'Les arguments doivent être des entiers, ici on a reçu a={a} et b={b}')
     22 return a + b

Exception: Les arguments doivent être des entiers, ici on a reçu a=1 et b=2.2

La ligne suivante permet d’activer le rendu du module doctest sous Jupyter.

%doctest_mode ON
Exception reporting mode: Plain
Doctest mode is: ON

Il suffit, pour tester le code, de mettre, par exemple à la fin du fichier les deux lignes suivantes. L’option verbose=True permet d’avoir des détails.

from doctest import testmod
testmod(verbose=True)
Trying:
    addition(1,2)
Expecting:
    3
ok
Trying:
    addition(1,2.2)
Expecting:
    Traceback (most recent call last):
    Exception: Les arguments doivent être des entiers, ici on a reçu a=1 et b=2.2
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.addition
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
TestResults(failed=0, attempted=2)

Sans le verbose=True, on a l’afichage suivant :

testmod()
TestResults(failed=0, attempted=2)

On peut documenter/tester des classes aussi :

class Personnage:
    """Une classe pour représenter une personne

    Exemple
    >>> joueur1 = Personnage()
    >>> joueur1.get_point_de_vie()
    100
    """
    def __init__(self):
        self.point_de_vie = 100
    
    def get_point_de_vie(self):
        return self.point_de_vie
testmod(verbose=True)
Trying:
    joueur1 = Personnage()
Expecting nothing
ok
Trying:
    joueur1.get_point_de_vie()
Expecting:
    100
ok
3 items had no tests:
    __main__
    __main__.Personnage.__init__
    __main__.Personnage.get_point_de_vie
1 items passed all tests:
   2 tests in __main__.Personnage
2 tests in 4 items.
2 passed and 0 failed.
Test passed.
TestResults(failed=0, attempted=2)

Attention, il faut prendre en compte que doctest fait de la comparaison de sortie standard, ce qui le rend assez fragile, celle-ci pouvant varier. De plus, lorsqu’il y a modification d’un objet muable [^muable].

def une_fonction_sans_return(L):
    """
    Une fonction qui modifie une liste
    L.sort()

    >>> L = [2, 1, 3]
    >>> une_fonction_sans_return(L)
    assert L == [1, 2, 3]
    """
testmod(verbose=True)
Trying:
    L = [2, 1, 3]
Expecting nothing
ok
Trying:
    une_fonction_sans_return(L)
Expecting:
    assert L == [1, 2, 3]
**********************************************************************
File "__main__", line 7, in __main__.une_fonction_sans_return
Failed example:
    une_fonction_sans_return(L)
Expected:
    assert L == [1, 2, 3]
Got nothing
1 items had no tests:
    __main__
**********************************************************************
1 items had failures:
   1 of   2 in __main__.une_fonction_sans_return
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=2)

4.2. Un cadriciel de test parmi d’autres#

Le but de ce cours n’est pas de faire un état des lieux exhaustifs de tous les cadriciels de test de Python. On peut toutefois citer :

Dans le cours de NSI, on va brièvement présenter pytest, ainsi que son utilisation rudimentaire lors des projets.

Pour les tests avec pytest, le mot clef [3] assert sera assez massivement utilisé, à l’intérieur d’une fonction.

def test_addition():
    assert addition(2,3) == 5

Généralement, on place les tests dans un répertoire tests en organisant un fichier test_<nom> pour chaque fichier nom.py. La commande suivante, lancée depuis un shell d’exécution permet de lancer les tests.

#!pip install pytest
!py.test-3 cours/tests/

Avec les bonnes bibliothèques, on peut assi l’utiliser dans un notebook :

#!pip install ipytest
import ipytest
ipytest.autoconfig()
%%ipytest
import pytest
def test_addition_int():
    assert addition(2,3) == 5

def test_addition_float():
    with pytest.raises(Exception):
      addition(2,2.2)

def addition(a, b):
    if not (isinstance(a,int) and isinstance(b, int)):
        raise Exception(f'Les arguments doivent être des entiers, ici on a reçu a={a} et b={b}')
    return a + b
.
.
                                                                                           [100%]
2 passed in 0.02s

De la même façon qu’avec doctest, on peut aussi tester des classes.

%%ipytest
def test_creation_personnage():
  joueur = Personnage()
  assert isinstance(joueur,Personnage)

def test_personnage_get_point_de_vie():
  joueur = Personnage()
  assert joueur.get_point_de_vie() == 100

class Personnage:
    def __init__(self):
        self.point_de_vie = 100

    def get_point_de_vie(self):
        return self.point_de_vie
.
.
                                                                                           [100%]
2 passed in 0.02s

L’écriture des tests, et des fonctions qui doivent passer les tests fournira ainsi un cadre robuste à la production de code. Les propositions, auss bien avec doctest qu’avec pytest préconisent de fournir les tests en amont (traduction de la demande), puis de coder les fonctions correspondantes.

4.3. Exercices#

  1. Écrire un test permettant de vérifier qu’une fonction renvoie bien un entier.

  2. Écrire un test pour vérifier que la fonction sorted permet de renvoyer une liste triée.

  3. Écrire un test pour vérifier que la fonction mystere appliquée à L, une liste de longueur len(L) renvoie bien

    1. un tuple

    2. contenant un entier entre 0 et et len(L) - 1

    3. un élément de la liste L.

  4. On considère le code suivant :

    def addition(a,b):
        return a,b
    
    def test_addition():
        assert addition(2,3) == 5
    

    Indiquer si la fonction est valide au regard du test proposé.