Tester un code et évaluer son efficacité, repérer et corriger ses erreurs.
Tests sur les entrées d'un code : les assertions
Lorsqu'on écrit un programme, on ne peut jamais prévoir toutes les situations possibles qui se produiront lorsque ce programme s’exécutera, et particulièrement toutes les valeurs possibles que pourra entrer par exemple l'utilisateur du programme et celles des arguments passés à une fonction.
Or, un code est prévu pour fonctionner normalement avec des paramètres d'un certain type, voire d'une certaine valeur bien précise !
Il est alors possible, dans l'écriture d'un code, de directement spécifier quelles préconditions doivent être respectées pour une bonne exécution du code, ce sont les assertions ( de manière générale, une assertion est une chose qui doit être considérée comme vraie. ).
Si ces préconditions ne sont pas vérifiées, alors le programme s'arrête ( il lève une exception ); au programmeur de faire alors en sorte que les préconditions soient toujours vérifiées, en rajoutant par exemple des tests sur les valeurs des paramètres avant l’exécution du code.
Prenons l'exemple d'une fonction qui calcule l'inverse d'un nombre entier positif :
def inverse(n):
'''
Calcule l'inverse d'un entier positif
Entrée : un entier n entier strictement positif
Sortie : un flottant, inverse de n
'''
return 1/n
Un utilisateur "lambda" ne consultera pas forcément la docstring de la fonction ( spoiler : il ne le fera jamais... ), et il sera libre d'entrer n'importe quelle valeur, qui ne respectera donc pas forcément les préconditions ( n entier strictement positif ).
On peut alors rajouter des assertions avant l’exécution du code de la fonction :
def inverse(n):
'''
Calcule l'inverse d'un nombre positif
Entrée : un entier n strictement positif
Sortie : un flottant, inverse de n
'''
assert n != 0 , "Le nombre ne doit pas être nul."
assert n > 0 , "Le nombre doit être positif."
assert isinstance(n, int) , "Le nombre doit être un entier."
return 1/n
Ces assertions lèveront une exception et afficheront un message si une des préconditions n'est pas respectée :
>>> inverse(12)
0.08333333333333333
>>> inverse(0)
Traceback (most recent call last):
File "", line 1, in
File "test.py", line 9, in inverse
assert n != 0 , "Le nombre ne doit pas être nul."
AssertionError: Le nombre ne doit pas être nul.
>>> inverse(-2)
Traceback (most recent call last):
File "", line 1, in
File "test.py", line 10, in inverse
assert n > 0 , "Le nombre doit être positif."
AssertionError: Le nombre doit être positif.
>>> inverse(1.2)
Traceback (most recent call last):
File "", line 1, in
File "test.py", line 11, in inverse
assert isinstance(n, int) , "Le nombre doit être un entier."
AssertionError: Le nombre doit être un entier.
>>>
La syntaxe est :
assert expression_booléenne, "Texte affiché si l 'assertion est fausse."
de nombreuses possibilités existent pour l'expression booléenne à tester :
-
test avec les opérateurs classiques :
assert n <= 5, "la valeur de 'n' doit être inférieure ou égale à 5." -
Appartenance à un type spécifié :
assert isinstance(n, int), "La variable 'n' doit être un entier." -
Appartenance à une liste de valeurs
assert n in [1,2,3], "La valeur de 'n' doit être comprise entre 1 et 3."
-
def interface(): req = str(input("voulez vous saisir un titre de film, un acteur ou un réalisateur ? ")) if req == "titre": print('appel de la fonction effectuant une requete sur un titre de film') elif req == "acteur": print('appel de la fonction effectuant une requete sur un acteur') elif req == "réalisateur": print('appel de la fonction effectuant une requete sur un réalisateur') -
def division_euclidienne(a, b): q=a//b r=a%b return q,r -
def dec_vers_bin(n,bits): '''convertit le nombre n en binaire sur un nombre de bits''' binaire="" for a in range(bits): binaire=str(n%2)+binaire n=n//2 return binaire
Les assertions sont alors regroupées dans une fonction de test.
Par exemple si on veut tester la fonction
somme() ci-dessous :
def somme(a,b):
return a+b
On peut imaginer la fonction de test :
def test_somme():
assert somme(0,0)==0
assert somme(1,1)==2
assert somme(1,-1)==0
assert somme(12,3)==15
assert somme(12,-3)==9
assert somme(50,100)==150
assert. Nous allons voir maintenant comment réaliser de vrais tests unitaires
Tests du résultat d'une fonction : les tests unitaires
La notion de tests est fondamentale en informatique : certains systèmes ne peuvent se contenter d'un fonctionnement approximatif mais doivent au contraire être robustes, c'est à dire fonctionner correctement dans toutes les situations possibles.
Pour prouver qu'une fonction fait toujours correctement le travail pour lequel elle est prévue, il faudrait théoriquement la tester avec tous les arguments possibles et imaginables; c'est bien entendu impossible...
On peut cependant se contenter de tester son bon fonctionnement sur quelques arguments bien choisis : on parle alors de tests unitaires
En Python, on peut utiliser le module doctest pour réaliser ces tests unitaires; il permet d'indiquer dans la docstring de la fonction des tests à réaliser et le résultat attendu si elle fonctionne bien.
Si ce n'est pas le cas, un message est alors affiché signalant qu'il faut corriger son code !
Voici un exemple avec une fonction qui donne le quotient et le reste de deux entiers :
import doctest
def division( n1, n2 ):
"""
Fonction pour calculer le quotient et le reste de la division de deux nombres.
Entrée :
deux nombres n1, n2
Sortie :
deux entiers dans l'ordre : quotient, reste de la division de n1 par n2
Tests unitaires :
>>> division(1,1)
(1, 0)
>>> division(2,1)
(2, 0)
>>> division(10,3)
(3, 1)
"""
quotient = n1 // n2
reste = n1 % n2
return quotient, reste
# Programme principal
doctest.testmod() # exécution des test unitaires
Dans la docstring, un test unitaire correspond aux 3 chevrons (>>> ) suivis de l'appel de la fonction avec des arguments particuliers; on indique en dessous le résultat attendu.
Si les tests unitaires sont validés, rien ne s'affiche. Par contre, en cas de mauvais fonctionnement, un ou plusieurs avertissement(s) s'affichent indiquant quel(s) test(s) n'ont pas été réussi(s).
Exemple avec la fonction précédente buguée :
import doctest
def division( n1, n2 ):
"""
Fonction pour calculer le quotient et le reste de la division de deux nombres.
Pré :
deux nombres n1, n2
Post :
deux entiers dans l'ordre : quotient, reste de la division de n1 par n2
Tests unitaires :
>>> division(1,1)
(1, 0)
>>> division(2,1)
(2, 0)
>>> division(10,3)
(3, 1)
"""
quotient = n1 / n2 # LA FONCTION EST BUGUÉE !!!
reste = n1 % n2
return quotient, reste
# Programme principal
doctest.testmod() # exécution des test unitaires
*********************************************************************
File "test.py", line 14, in __main__.division
Failed example:
division(1,1)
Expected:
(1, 0)
Got:
(1.0, 0)
**********************************************************************
File "test.py", line 17, in __main__.division
Failed example:
division(2,1)
Expected:
(2, 0)
Got:
(2.0, 0)
**********************************************************************
File "test.py", line 20, in __main__.division
Failed example:
division(10,3)
Expected:
(3, 1)
Got:
(3.3333333333333335, 1)
**********************************************************************
1 items had failures:
3 of 3 in __main__.division
***Test Failed*** 3 failures.
>>>
La fonction renvoyant un flottant alors que c'est un entier qui est attendu, une erreur est signalée.
Une méthode de développement appelé TDD préconise d'ailleurs d'écrire D'ABORD des tests avant même le code d'une fonction...
-
def somme (N): somme = 0 for i in range(1, N+1): somme = somme + i return somme -
def dec_vers_bin(n,bits): '''convertit le nombre n en binaire sur un nombre de bits''' binaire="" for a in range(bits): binaire=str(n%2)+binaire n=n//2 return binaire
Rechercher et analyser les erreurs d'un code
Analyse des messages d'erreur de la console
Lorsqu'un programme ne s'exécute pas totalement, la console python nous fourni un message d'erreur.L'analyse de ces message (en anglais...) nous permet souvent de gagner beaucoup de temps (recherche fastidieuse d'une parenthèse, de deux points manquants).
Il faut cependant savoir et comprendre ces messages.
Les petites erreurs à éviter
Dans le code ci-dessous vous voyez sûrement l'erreur évidente de synthaxe
L=['toto',2,1.23,True
a=1
Cependant, la console python nous indiquera dans ce cas le message suivant :
File "test.py", line 2
a=1
^
SyntaxError: invalid syntax
Il semble difficile de déceler une erreur à la ligne indiquée par la console python (pas d'erreur de syntaxe dans a=1 )Dans ce cas il faut chercher avant l'indication de la console.
En effet l'interpréteur de python attend le crochet fermant de la liste
L et bute sur le signe =.
Décodage des messages d'erreur
Vous devez produire un code pour chaque message d'erreur proposé ci-dessous. Votre code devra générer le message proposé.
1. IndexError: list index out of range
2. SyntaxError: invalid syntax
3. TypeError: 'int' object is not subscriptable
4. NameError: name 'variable' is not defined
5. SyntaxError: EOL while scanning string literal
6. TypeError: unsupported operand type(s) for /: 'str' and 'str'
7. ValueError: invalid literal for int() with base 10: '12.5'
8. TypeError: object of type 'float' has no len()
9. IndentationError: unexpected indent
10. IndentationError: expected an indented block
11. IndentationError: unindent does not match any outer indentation level
12. TypeError: f1() takes 0 positional arguments but 1 was given
13. TypeError: f2() takes 1 positional argument but 2 were given
14. ValueError: invalid literal for int() with base 10: '12.5'
Affichage de la trace des variables
Il peut arriver qu'un programme s'exécute normalement mais ne réalise pas la tache demandée. Il est alors nécessaire de trouver la ou les erreurs algorithmiques présentes dans le code...Malheureusement, la plupart du temps, regarder le code "dans les yeux" ne suffit pas.il faut donc :
- Relire une fois attentivement son code en lien avec le déroulement de l'algorithme prévu. Si cela ne vous a pas permis de résoudre tous les problèmes...
- Identifier les variables cruciales de votre code (pas plus de 2 ou 3).
- Afficher ces variables grâce à la commande
printen précisant à chaque fois quelle variable est affichée (print('i=',i)plutôt que print(i) et en commentant cet affichage de test(#test) - Analyser ces affichages et recommencer à la première étape
from random import randint
nombre = randint( 1 , 100
entree = 0
while ( entree == nombre ) :
entree = int(input('Entrez votre proposition :'))
if ( entree > nombre )) :
print('Trop grand !')
elif ( entree < nombre )
print('Trop petit !')
print('Bravo !')
Évaluer l'efficacité
Si on veut par exemple comparer l'efficacité de deux programmes différents, on peut vouloir mesurer leur temps d'exécution.
Un module existe pour cela, le module timeit. Par exemple, pour mesurer la durée d’exécution d'une fonction :
from timeit import timeit
def test(n):
"""Fonction inutile"""
L = [i for i in range(n)]
t = timeit(stmt = 'test(1000)', globals=globals(), number = 100)
print(t)
- stmt est une chaîne de caractères donnant le bout de code à exécuter ( ici, l'appel à la fonction
test()avec un paramètre n égal à 1000 ) - number est le nombre de répétitions de l’exécution du code
Le module mesure la durée moyenne ( en secondes ) de l’exécution de la fonction test() sur plusieurs exécutions successives ( cette durée est souvent influencée par de nombreux paramètres extérieurs au code ).