Tester☘
Toute fonction (et tout programme) doit être testée.
Des tests nombreux et bien choisis ne garantiront jamais que le programme est exempt de bugs mais permettent de s'assurer que la fonction n'est pas trivialement fausse et qu'elle semble fonctionner dans les cas usuels.
Si les tests ne sont jamais une preuve de l'absence de bugs, vous devrez penser la construction de vos tests de façon à ce qu'ils débusquent les bugs, en pensant notamment à tous les cas particuliers qu'un premier raisonnement générique aurait pu oublier.
Exemple☘
On veut écrire une fonction spécifiée ainsi:
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2**n > m.
"""
Cassien propose le code suivant:
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2**n > m.
"""
p = 1
n = 0
while p < m:
p = p*2
n = n+1
return n
Il teste ensuite sa fonction avec l'argument 3:
>>> pepg(3)
2
et estime que son code est correct puisque 22 > 3 et que 21 ≤ 3.
Qu'en pensez-vous?
Aide: un test qui échoue.
Quelques tests supplémentaires montrent que ce code n'est pas correct.
Lorsqu'on écrit une fonction, il faut toujours tester les cas limites.
Ici, on doit notamment penser à ce qui peut se passer:
- pour les premiers entiers (0, 1, 2).
- pour tous les cas où il y a égalité dans les comparaisons.
On constate notamment ici que la fonction ne satisfait pas sa spécification lorsque m est une puissance de deux (c'est un cas où p prendra exactement la valeur de m lors de l'une des itérations). On obtient en effet:
>>> pepg(4)
2
>>> pepg(8)
3
alors que l'on n'a pas 2^2 > 4, ni 2^3 > 8.
Essayez de trouver comment corriger le code avant de lire la solution.
Correction du code
Le test while p < m
doit être corrigé
en while p <= m
.
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2^n > m.
"""
p = 1
n = 0
while p <= m:
p = p*2
n = n+1
return n
Tests de documentation☘
Il vous sera demandé en cours de NSI de renseigner systématiquement le docstring de vos fonctions et de le compléter par quelques tests et les résultats attendus, en suivant la syntaxe donnée en exemple ci-dessous.
Les tests couvriront notamment les cas:
- cas génériques.
- cas extrêmes.
Ici, on pourrait par exemple compléter comme suit (des cas "génériques" avec 3, 5; des cas extrêmes: les premiers entiers naturels: 0, 1; des cas avec égalité dans les tests de comparaison: puissances de deux 1, 2, 4).
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2^n > m.
>>> pepg(0)
0
>>> pepg(1)
1
>>> pepg(2)
2
>>> pepg(3)
2
>>> pepg(4)
3
>>> pepg(5)
3
"""
p = 1
n = 0
while p <= m:
p = p*2
n = n+1
return n
Remarque: la syntaxe des tests de documentation
Le choix de la présentation
>>> pepg(0)
0
>>> pepg(1)
1
>>> pepg(2)
2
>>> pepg(3)
2
>>> pepg(4)
3
>>> pepg(5)
3
Par ailleurs, nous verrons ci-dessous que ces tests de la chaîne de documentation peuvent être utilisés directement par le module doctest. Et en fait le module doctest se contente de comparer ces tests avec ce que l'on obtiendrait effectivement dans une console. La syntaxe à respecter pour ces tests sera donc guidée par ce que l'on attend comme affichages en console.
Lancer les tests de documentation depuis la console
Cette convention des tests dans le docstring permet de mieux comprendre la spécification pour le lecteur.
Pour le programmeur, qui pensera ces tests avant de programmer, elle est aussi un guide
pour ne pas oublier de cas.
Enfin, elle permet aussi des tests directs grâce au module doctest.
Enregistrons par exemple le script ci-dessus (en réintroduisant l'erreur sur la condition d'arrêt du while) dans un fichier nommé essai.py.
Contenu du fichier essai.py:
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2^n > m.
>>> pepg(0)
0
>>> pepg(1)
1
>>> pepg(2)
2
>>> pepg(3)
2
>>> pepg(4)
3
>>> pepg(5)
3
"""
p = 1
n = 0
while p < m:
p = p*2
n = n+1
return n
Ouvrons maintenant une console dans le répertoire de notre fichier et entrons la commande suivante:
$ python3 -m doctest -v essai.py
(rappel: n'entrez pas le $, il s'agit de l'invite de commande normalement déjà présente).
Vous devriez obtenir la liste des tests réussis et des tests échoués:
Trying:
pepg(0)
Expecting:
0
ok
Trying:
pepg(1)
Expecting:
1
**********************************************************************
File "/home/jmm/Documents/NSI/coursNSI/for-if-while/docs/essai.py", line 8, in essai.pepg
Failed example:
pepg(1)
Expected:
1
Got:
0
Trying:
pepg(2)
Expecting:
2
**********************************************************************
File "/home/jmm/Documents/NSI/coursNSI/for-if-while/docs/essai.py", line 10, in essai.pepg
Failed example:
pepg(2)
Expected:
2
Got:
1
Trying:
pepg(3)
Expecting:
2
ok
Trying:
pepg(4)
Expecting:
3
**********************************************************************
File "/home/jmm/Documents/NSI/coursNSI/for-if-while/docs/essai.py", line 14, in essai.pepg
Failed example:
pepg(4)
Expected:
3
Got:
2
Trying:
pepg(5)
Expecting:
3
ok
1 items had no tests:
essai
**********************************************************************
1 items had failures:
3 of 6 in essai.pepg
6 tests in 2 items.
3 passed and 3 failed.
***Test Failed*** 3 failures.
Le module doctest repère les tests grâce à la présence des chevrons >>>
, il est donc
impératif de respecter cette syntaxe.
En supprimant l'option -v
:
python3 -m doctest essai.py
on n'obtient que la liste des échecs (et donc aucun affichage si tous les tests réussissent).
Pour en savoir plus sur le module doctest, consulter la documentation python.
Lancer les tests de documentation avec le fichier .py
Nous avons vu dans le cadre précédent comment lancer les tests de la chaîne de documentation depuis une console.
On peut également intégrer directement dans son fichier .py de quoi lancer les tests.
Reprenons l'exemple ci-dessus en lui ajoutant une fonction permettant de lancer les tests doctest.
def test_pepg():
"""
fonction lançant les tests de la chaîne de documentation
de la fonction pepg.
"""
import doctest
doctest.testmod(verbose=True)
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2^n > m.
>>> pepg(0)
0
>>> pepg(1)
1
>>> pepg(2)
2
>>> pepg(3)
2
>>> pepg(4)
3
>>> pepg(5)
3
"""
p = 1
n = 0
while p <= m:
p = p*2
n = n+1
return n
test_pepg()
assert
Vous pourrez dans la suite définir vos tests avec assert
si vous préférez:
le seul point incontournable est de prévoir un jeu de tests.
On pourrait par exemple prévoir la fonction de tests suivante:
def test_pepg():
"""
fonction lançant des tests
sur la fonction pepg
"""
assert pepg(0) == 0
assert pepg(1) == 1
assert pepg(2) == 2
assert pepg(3) == 2
assert pepg(4) == 3
assert pepg(5) == 3
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2^n > m.
>>> pepg(0)
0
>>> pepg(1)
1
>>> pepg(2)
2
>>> pepg(3)
2
>>> pepg(4)
3
>>> pepg(5)
3
"""
p = 1
n = 0
while p < m:
p = p*2
n = n+1
return n
et la lancer, par exemple, dans le terminal:
>>> test_pepg()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "a.py", line 7, in test_pepg
assert pepg(1) == 1
AssertionError
On constate une erreur d'assertion AssertionError
(puisqu'on a exécuté ici
avec < au lieu de <= dans la comparaison)
Une fonction de tests☘
L'utilisation d'une fonction dédiée aux tests permet parfois de compléter les tests de la chaîne de documentation par d'autres tests (notamment lorsque ces tests demandent un texte conséquent et qu'ils surchargeraient trop la chaîne de documentation).
Exemple☘
Dans l'exemple ci-dessous, la fonction de tests contient une assertion en plus de lancer les tests de la chaîne de documentation de la fonction à tester.
Cette assertion ne provoquera aucun affichage si elle est satisfaite.
- Lancez le script tel quel. Vous ne devriez n'avoir ici aucun affichage car tous les tests passent.
- Lancez ensuite le script en remplaçant la dernière ligne par
test_pepg(True)
. Vous devriez obtenir un compte-rendu des tests de la chaîne de documentation. - Enfin, remplacez l'assertion de la fonction de tests par une assertion incorrecte (par exemple
assert pepg(8) == 100
). Vous devriez obtenir une "AssertionError".
def test_pepg(bavard = False):
"""
fonction lançant les tests de la chaîne de documentation
de la fonction pepg.
"""
assert pepg(8) == 4
import doctest
doctest.testmod(verbose=bavard)
def pepg(m):
"""
m -- entier naturel
renvoie le plus petit entier naturel n tel que 2^n > m.
>>> pepg(0)
0
>>> pepg(1)
1
>>> pepg(2)
2
>>> pepg(3)
2
>>> pepg(4)
3
>>> pepg(5)
3
"""
p = 1
n = 0
while p <= m:
p = p*2
n = n+1
return n
test_pepg()
Remarque: les métiers du test
Vous aurez noter une petite difficulté pour un débutant: la fonction de tests doit elle-même
être dépourvue d'erreurs!
Tester du logiciel est en fait devenu aujourd'hui un métier à part entière, cela demande des
compétences poussées et un travail qui n'est jamais négligeable et qui ne doit jamais être négligé
dans les phases de développement logiciel.
Remarque: les modules de tests
L'importance des tests fait qu'il existe dans quasiment tous les langages des modules dédiés aux tests. En python, nous avons déjà cité doctest, il existe aussi unittest et pytest. Ces modules demandent à ce que les fonctions de tests respectent certaines règles d'écriture... En contrepartie, ils permettent d'automatiser les tests, de repèrer les erreurs, etc... Vous pouvez par exemple lire cette page sur pytest.
Assertion☘
Un autre risque d'erreur est lié à l'appel des fonctions avec un argument qui n'est pas du type attendu.
On peut ajouter des assertions pour vérifier le respect des préconditions sur les paramètres lors des appels à la fonction et ainsi anticiper ce type d'erreur.
Avec notre fonction précédente, on peut par exemple ajouter un assert
pour s'assurer que la fonction
est appelée avec un m entier positif ou nul.
On utilise ici isinstance pour tester le type de m.
(on a supprimé la chaîne de documentation pour allèger... mais ce n'est pas un exemple à suivre ! les chaînes de documentation devront toujours être présentes !)
def pepg(m):
assert isinstance(m, int), "Attention, le paramètre doit être entier."
assert m >= 0, "Attention, le paramètre doit être positif ou nul."
p = 1
n = 0
while p <= m:
p = p*2
n = n+1
return n
Testez cette fonction
- avec un entier positif,
- avec un entier négatif,
- avec un flottant
et observez les réponses obtenues.