Doctest : une façon originale et pratique de tester…

Objectifs

  • utiliser un outil de test : doctest
  • établir une méthodologie pour les TP

Motivation

Vous savez documenter les fonctions à l’aide d’une «chaîne de documentation» (ou «docstring»), c’est-à-dire une chaîne de caractères placée immédiatement après l’en-tête de la fonction. Voici un tel exemple de documentation

def fact(n):
    """
        paramètre n : (int) un entier
        valeur renvoyée : (int) la factorielle de n.

    CU : n >= 0

    Exemples :

    >>> fact(3)
    6
    >>> fact(5)
    120
    """
    res = 1
    for i in range(2, n + 1):
       res = res * i
    return res

Cette documentation peut être exploitée avec la fonction help :

>>> help(fact)
Help on function fact in module __main__:

fact(n)
        paramètre n : (int) un entier
        valeur renvoyée : (int) la factorielle de n.

    CU : n >= 0

    Exemples :

    >>> fact(3)
    6
    >>> fact(5)
    120

À faire

Utilisez Pyzo pour

  1. recopier la fonction fact avec sa docstring dans un fichier que vous nommerez exples_doctest.py,
  2. et utiliser la fonction help au niveau de l’interpréteur.

Réaliser une telle chaîne de documentation permet

  • à l’utilisateur de la fonction de savoir
    • à quoi peut servir la fonction ;
    • comment il peut l’utiliser ;
    • et quelles conditions il doit respecter pour l’utiliser (CU).
  • et au programmeur de la fonction de préciser
    • le nombre et la nature de ses paramètres ;
    • la relation entre la valeur renvoyée et celle du ou des paramètres ;
    • ses idées avec quelques exemples.
    (Tout cela bien entendu à condition que cette documentation soit rédigée avant la réalisation du programme et non le contraire.)

Mais vous allez découvrir que cela permet davantage encore !

Utiliser le module doctest

Les exemples donnés dans une chaîne de documentation peuvent être testés à l’aide d’un module de Python nommé doctest.

À faire

Depuis l’interpréteur (shell), dans lequel la fonction fact ci-dessus est supposée chargée, tapez les deux lignes

>>> import doctest

pour importer le module, et

>>> doctest.testmod()

Vous devez obtenir

>>> doctest.testmod()
TestResults(failed=0, attempted=2)

La fonction testmod du module doctest est allée chercher dans les docstring des fonctions du module actuellement chargé, c’est-à-dire exples_doctest, tous les exemples (reconnaissables à la présence des triples chevrons >>>), et a vérifié que la fonction documentée satisfait bien ces exemples. Dans le cas présent, une seule fonction dont la documentation contient deux exemples (attempted=2) a été testée, et il n’y a eu aucun échec (failed=0).

Et si un exemple et la fonction ne sont pas d’accord ?

À faire

Modifiez le deuxième exemple, en mettant 121 à la place de 120 dans le second exemple. Chargez le fichier dans l’interpréteur (touche F5) et retapez les deux lignes

>>> import doctest
>>> doctest.testmod()

Vous devez obtenir

>>> doctest.testmod()
**********************************************************************
File "/home/eric/AP1/exples_doctest.py", line 24, in __main__.fact
Failed example:
    fact(5)
Expected:
    121
Got:
    120
**********************************************************************
1 items had failures:
   1 of   2 in __main__.fact
***Test Failed*** 1 failures.

Qu’est ce que tout cela révèle ?

  • Tout d’abord que les tests ont échoué et qu’il y a eu 1 échec (cf dernière ligne) et que cet échec est dû à la fonction fact (cf avant dernière ligne).
  • Ensuite que le test incriminé est celui concernant fact(5) pour lequel le test a obtenu (Got) 120 en exécutant la fonction fact, alors qu’il attendait (Expected) 121 selon l’exemple donné par la documentation.

Lorsqu’il y a de tels échecs, cela invite le programmeur à vérifier son programme, … ou bien les exemples de sa documentation, comme c’est le cas ici.

Rendre automatique les tests

Il est très facile de rendre automatique les tests et ainsi de ne plus avoir à faire appel explicitement (et manuellement) à la fonction testmod.

Il suffit pour cela d’inclure en fin de fichier les trois lignes :

if __name__ == '__main__':
    import doctest
    doctest.testmod()

À faire

Ajoutez ces trois lignes à la fin du fichier exple_doctest.py et exécutez-le ! (F5) Faites le dans le cas d’un test erroné, et dans le cas sans erreur.

Que remarquez-vous dans le cas sans erreur ?

Rendre les doctests bavards même en cas de succès

Un paramètre optionnel de la fonction testmod permet d’obtenir plus d’informations sur les tests effectués mêmes en cas de succès.

Il suffit pour cela de rajouter le paramètre ̀`verbose=True«  :

doctest.testmod(verbose = True)

À faire

Faites-le ! Et observez ce que vous obtenez

  1. avec des exemples erronés
  2. sans exemple erroné.

Les sorties complexes

Tester les exemples des docstring avec le module doctest peut être source de déboires et de pièges. Vous allez découvrir certains d’entre eux et les remèdes qu’on peut y apporter.

Avec les listes

Supposez que vous vouliez donner un exemple qui produit la liste des factorielles des entiers de 0 à 5. Vous avez donc complété votre documentation en ajoutant

"""
paramètre n : (int) un entier
valeur renvoyée : (int) la factorielle de n.


CU : n >= 0

Exemples :

>>> fact(3)
6
>>> fact(5)
120

La liste des factorielles des entiers de
0 à 5
>>> [fact(n) for n in range(6)]
[1,1,2,6,24,120]
"""

Avertissement

La ligne blanche entre le deuxième exemple et la phrase qui suit est absolument nécessaire. En son absence, la phrase sera comprise comme faisant partie de la sortie produite par le deuxième exemple, et le test échouera donc.

Il est clair que ce nouvel exemple est tout à fait correct. Pourtant, si vous procédez au test vous constaterez que sur les trois exemples testés, l’un a abouti à un échec : le troisième.

À faire

Faites-le !

Quel est le problème ? Cela vient du fait que la fonction testmode effectue une comparaison litérale entre la réponse fournie par la documentation (Expected ) et celle fournie par l’interpréteur (Got).

À faire

Examinez attentivement ces deux points (Expected et Got) dans la réponse du test que vous venez d’effectuer.

Avez-vous compris ? Le problème, ce sont les espaces que l’interpréteur place après chaque virgule dans l’énumération des éléments de la liste. Dans la documentation, ils n’y sont pas.

Comment corriger ce point ? C’est simple, il faut mettre des espaces entre les éléments d’une liste.

Mais ce n’est pas si simple. On peut facilement mettre plusieurs espaces, comme ci-dessous :

"""
paramètre n : (int) un entier
valeur renvoyée : (int) la factorielle de n.

CU : n >= 0

Exemples :

>>> fact(3)
6
>>> fact(5)
120

La liste des factorielles des entiers de
0 à 5
>>> [fact(n) for n in range(6)]
[1,  1, 2,   6,  24,   120]
"""

À faire

Faites-le ! Testez !

L’excès d’espaces provoque des erreurs. Si on ajoute, sous forme d’un commentaire la directive #doctest :

+ NORMALIZE WHITESPACE , alors le test réussit, à condition néanmoins d’avoir mis au moins une espace après chaque virgule.

"""
paramètre n : (int) un entier
valeur renvoyée : (int) la factorielle de n.

CU : n >= 0

Exemples :

>>> fact (3)
6
>>> fact (5)
120

La liste des factorielles des entiers de
0 à 5
>>> [fact (n) for n in range (6)]
... # doctest: +NORMALIZE_WHITESPACE
[1,  1, 2,   6,  24,   120]
"""

À faire

Vérifiez-le !

Avertissement

les trois petits points sous les trois chevrons sont indispensables.

Avec la directice supplémentaire +ELLIPSIS , on peut même se dispenser d’énumérer explicitement tous les éléments de la liste :

"""
paramètre n : (int) un entier
valeur renvoyée : (int) la factorielle de n.

CU : n >= 0

Exemples :

>>> fact(3)
6
>>> fact(5)
120

La liste des factorielles des entiers de
0 à 5
>>> [fact(n) for n in range(6)]
... # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS
[1,  ...,   24,   120]
"""

À faire

Vérifiez-le !

Attention aux ensembles et dictionnaires

Contrairement aux listes ou tuples, les ensembles et les dictionnaires ne sont pas des structures de données séquentielles. Il est impossible de prévoir dans quel ordre un interpréteur Python écrira les éléments de ces structures.

Par exemple, on peut très bien avoir

>>> {'a', 'b'}
{'a', 'b'}

comme on peut avoir

>>> {'a', 'b'}
{'b', 'a'}

Il est donc difficile d’illustrer une valeur d’un de ces deux types dans un exemple d’une docstring. Il est préférable de tester l’égalité de deux valeurs [1] :

>>> {'a', 'b'} == {'b', 'a'}

Voici donc un exemple de ce qu’il est envisageable de placer dans une docstring

Un dictionnaire de certaines valeurs de fact :
>>> set (n for n in range(5)) == {0, 1, 2, 3, 4}
True

À faire

Vérifiez ce point !

Avec des sorties aléatoires

Comment tester des fonctions qui produisent des valeurs aléatoires ?

Dans l’absolu, il est impossible de placer un exemple dans la docstring donnant le résultat d’un appel à de telles fonctions puisque les valeurs qu’elles renvoient sont imprévisibles.

Par exemple si on veut tester qu’une fonction simulant un dé à six faces ne produit que des nombres compris entre 1 et 6, comment faire ?

On peut si on le souhaite vérifier que cette fonction ne renvoie que des nombres compris entre 1 et 6.

from random import randrange

def de():
    """
        paramètre : aucun
    valeur renvoyée : (int) un nombre choisi au hasard compris entre
    1 et 6.

    CU : aucune

    Exemple :

    >>> 1 <= de() <= 6
    True
    """
    return randrange(1,7)

À faire

Concevez un test qui vérifie 100 fois qu’aucun nombre produit par la fonction de n’est en dehors de l’intervalle [1,6]

.

Mais un tel test ne prouve pas que jamais la fonction ne produira un nombre en dehors de cet intervalle.

Méthodologie

Note

Pour chacun des fichiers de programmes Python que vous écrirez durant les séances de TP, ainsi que dans la réalisation du projet de fin de semestre, vous devrez

  • documenter toutes les fonctions avec une docstring donnant des exemples pertinents d’utilisation de ces fonctions
  • ajouter les trois lignes de code suivantes en fin de fichier : if __name__ == « __main__ »: import doctest doctest.testmod()

Pour en savoir plus

Voir la documentation de Python.

Notes

merci à l’université de Lilles pour son cours Algorithmes et Programmation 1.

[1]À condition qu’on puisse tester l’égalité des éléments de ces structures.