Quand on écrit un programme qui fait quelque chose de précis – par exemple résoudre une certaine équation – alors naturellement, à la fin du programme on obtient la réponse à la question et on voit tout de suite si le programme fonctionne ou non.
Mais très souvent, on ne fait que produire des fonctions que l'on utilise pas tout de suite. Si on exécute un module qui ne contient que des fonctions, Python ne pourra détecter que les erreurs de syntaxe et on ne saura pas si le programme fait vraiment ce qu'on attend de lui.
Dans ce cas, nous devons faire des tests afin de vérifier que les différents fonctions font bien ce que l'on veut.
À titre d'illustration, je propose un module composé de deux fonctions très simples bien sûr ce n'est qu'un exemple, ces deux fonctions n'ont pas grand intérêt !
"""
fichier monmodule.py
ce fichier est une bibliothèque de fonctions qui seront utilisées ailleurs,
dans le programme principal par exemple
"""
def moitie(x):
"""
x: un nombre
renvoie la moitié de x
"""
return x/2
def double(x):
"""
x:nombre
renvoie le double de x
"""
return 2*x
Comme déjà dit, si on exécute ce programme, il ne se passe rien : On n'a demandé aucune exécution. C'est normal, ce fichier n'est qu'un module qui définit les fonctions. Elles seront exécutées ailleurs. Pourtant nous devons les tester.
La première idée qui vient est d'exécuter les fonctions pour voir ce qu'elles font.
On exécute d'abord le module ce qui a pour effet de définir les fonctions. Elles sont maintenant utilisables en console. On peut alors tester en console :
>>> moitie(10) 5.0 >>> moitie(3) 1.5 >>> double(6) 12
Suite à cela, on est content, les fonctions ont l'air de faire ce qui est attendu. Mais c'est une très mauvaise façon de faire ! On peut faire cela pour une vérification rapide mais ce n'est pas viable dans le cadre d'un gros projet. Voici quelques arguments.
Le problème principal du cas précédent est qu'on doit tout refaire à chaque fois.
On pourrait être tenté de faire ceci :
"""
fichier monmodule.py
ce fichier est une bibliothèque de fonctions qui seront utilisées ailleurs,
dans le programme principal par exemple
"""
def moitie(x):
"""
x: un nombre
renvoie la moitié de x
"""
return x/2
def double(x):
"""
x:nombre
renvoie le double de x
"""
return 2*x
# et on ajoute les tests ici, les tests consistant en de simples
# exécutions des fonctions, avec un affichage
print(moitie(10))
print(moitie(3))
print(double(6))
Et l'exécution de ce code renvoie l'affichage :
5.0 1.5 12
C'est une très mauvaise idée ! En effet, si on déclare ainsi nos tests, ils s'exécuteront chaque fois que l'on ouvrira le module. Le simple fait d'importer – import monmodule – provoquera l'exécution des lignes de tests et alors on aura l'affichage des 3 lignes. Mais le programmeur qui utilise mon module n'est pas intéressé par mes tests, nos print le gênent. Les tests ne devraient s'exécuter que dans un contexte où on décide de faire des tests, pas tout le temps !
Voici ce que l'on peut faire à la place :
"""
fichier monmodule.py
ce fichier est une bibliothèque de fonctions qui seront utilisées ailleurs,
dans le programme principal par exemple
"""
def moitie(x):
"""
x: un nombre
renvoie la moitié de x
"""
return x/2
def double(x):
"""
x:nombre
renvoie le double de x
"""
return 2*x
# et on ajoute les tests ici, les tests consistant en de simples
# exécutions des fonctions, avec un affichage
if __name__ == '__main__':
print(moitie(10))
print(moitie(3))
print(double(6))
Le test fait une grande différence car il tient compte de la façon dont on exécute le contenu de ce fichier.
__name__ est égal à '__main__' et les tests s'exécutent.import monmodule – alors __name__ est égal à 'monmodule', la condition n'est pas validée et les tests ne s'exécutent pas.Puisque notre fichier est censé être un module, dans le cadre de son utilisation normale en tant que module – cas 2. – les tests ne sont pas exécutés. Mais dans au moment où on écrit le module, on peut l'exécuter en tant que principal et alors les tests sont bien exécutés.
On a ce qu'on voulait : un contexte normal dans lequel les tests ne s'exécutent pas et un contexte spécial de développement dans lequel les tests s'exécutent.
Néanmoins cette approche n'est toujours pas satisfaisante.
5.0. Est-ce juste ? Pour le savoir on est obligé de voir que 5.0 est le résultat de moitie(10) puis on doit déterminer si la bonne réponse de moitie(10) devrait bien être 5.0.
Au lieu d'afficher le résultat de moitie(10) et de se demander ensuite si le résultat affiché est le bon, on procède dans le bon ordre : On se pose d'abord la question de ce que devrait renvoyer moitie(10) ; quand on sait que c'est 5.0 on dit clairement au programme “moitie(10) devrait renvoyer 5.0” ce qui se traduit assert moitie(10) == 5.0.
Mes tests deviennent alors :
# tests avec assert
if __name__ == '__main__':
assert moitie(10) == 5.0
assert moitie(3) == 1.5
assert double(6) == 12
Cette approche est meilleure car la vérification est automatique.
assert est validé, il ne se passe rien, nous n'avons rien à faire de plus,asert non valide lève une erreur. Le programme s'arrête à la ligne problématique. On sait où cela coince.Mais cette approche a encore quelques défauts :
assert n'est pas validé, les tests s'interrompent. On ne voit pas en une seule fois l'ensemble des erreurs.Nous arrivons à la solution idéale : un fichier de tests.
Python – comme de nombreux langages – met à disposition un module unittest contenant de nombreuses fonctionnalités de tests. Ce module va nous permettre de définir une sorte de machine à tester nos fonctions.
# fichier monmodule.test.py
# import des fonctionnalités avancées de tests
import unittest
# import du module contenant les fonctions à tester
import monmodule
# création de la 'machine' à tester
class MyTest(unittest.TestCase):
def test1(self):
"""
Test pour moitie(10)
"""
x = moitie(10)
self.assertEqual(5.0, x)
def test2(self):
"""
Test pour moitie(3)
"""
x = moitie(3)
self.assertEqual(1.5, x)
def test3(self):
"""
Test pour double(6)
"""
x = double(6)
self.assertEqual(12, x)
if __name__ == '__main__':
# quand on exécute ce module de test, il déclenche l'ensemble des tests :
unittest.main()
Chaque fonction test correspond à un test qui sera exécuté.
On recevra un message clair disant pour chaque test
Avantages :
unittest met à disposition des tests plus avancés – liste complète.Les environnements de travail plus avancés comme VS Code sont capable de reconnaître les modules de test. L'environnement sera alors capable de mettre ces modules dans un endroit spécial et il les exécutera automatiquement de sorte que d'un coup d’œil, on verra quel test a réussi et quel test a échoué. Ainsi on n'aura beaucoup moins de peine à trouver la source de l'erreur.
On parle de test driven development – développement conduit par les tests. L'idée est que l'on programme en commençant par définir les tests à valider. On commence alors par écrire les modules de tests en essayant d'envisager tous les cas possibles d'utilisation. On programme ensuite les fonctions désirées et quand tous les tests sont validés, on a terminé.