Unittest et Python
Introduction
Ce billet est le premier d’une série de quatre ou cinq billets (normalement) consacré aux tests avec Python. Je les sépare car ils correspondent à des aspects différent les uns des autres et non pas basé sur une difficulté croissante d’un seul module.
Quelques mots pour les personnes n’aillant jamais écrit de tests. Il y a beaucoup d’avantages à en écrire, par exemple être plus confient lors d’amélioration de code existant, être sûr de ne rien « casser ». Il s’agit bien souvent (pour les tests unitaires du moins) de tester une petite partie d’un code, afin de s’assurer qu’on obtient les valeurs auxquelles on s’attendait. Ce billet n’est pas là pour faire une introduction sur les avantages de tester son code, mais sachez que c’est une pratique indispensable et courante pour tout code.
Dans ce billet, nous allons utiliser un module, unittest qui est disponible directement avec Python. J’aurais pu commencer la série avec doctest, également un module natif à Python, permettant d’écrire les tests directement sous forme de commentaire, pour les personnes intéressées, la documentation officiel est certainement un bon endroit pour commencer. Ce billet n’est qu’un rapide aperçu de unittest, et ne se veux pas d’être complet.
Le code
Commençons par un exemple très simple. En créant une fonction add()
qui…
additionne deux chiffres !
Créez un répertoire de test, dans lequel on crée la fonction suivante dans un
fichier chiffres.py
:
def add (a, b):
return a+b
On écrit maintenant le test correspondant :
import unittest
from chiffres import add
class TestChiffres(unittest.TestCase):
def test_addition(self):
result = add(36, 6)
self.assertEqual(result, 42)
if __name__ == '__main__':
unittest.main()
Quelques explications rapide:
On import le module pour unittest, ainsi que la fonction qu’on a crée.
from chiffres import add
On crée une classe qui doit commencer par Test, et héritant de unittest.TestCase, correspondant à la ligne suivante :
La fonction suivante est celle qui est utilisé pour le test, et doit commencer
par test_
pour qu’elle soit pris en compte. Le plus important, c’est de
vérifier la valeur que la fonction retourne avec celle auquel on s’attend, et
c’est ce que fais la ligne suivante :
La dernière partie, correspond à l’appel de la class par unittest :
unittest.main()
Content de notre premier test, on fait un essai :
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Maintenant, on veut s’assurer que la fonction lève une exception si donne comme argument une chaine de caractères, on écrit donc le test:
import unittest
from chiffres import add
class TestChiffres(unittest.TestCase):
def test_addition(self):
result = add(36, 6)
self.assertEqual(result, 42)
def test_add_string(self):
self.assertRaises(ValueError, add, 'coin', 'pan')
if __name__ == '__main__':
unittest.main()
On utilise assertRaises, avec comme argument, l’exception attendu, la fonction et une liste d’argument fournis à la fonction. On essaye le test :
F.
======================================================================
FAIL: test_add_string (__main__.TestChiffres)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_chiffres.py", line 13, in test_add_string
self.assertRaises(ValueError, add, 'coin', 'pan')
AssertionError: ValueError not raised by add
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Le test échoue, on s’y attendait un peu, puisque le résultat retourné est la concaténation des deux chaines, c’est à dire ‘coinpan’.
On écrit un bout de code qui lèvera une erreur en cas de chaines passé en argument, comme suit :
def add (a, b):
return int(a)+int(b)
On execute maintenant le test :
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Très bien, c’est ce qu’on voulait. Mais que ce passe-t-il si on envoie comme argument des floats (nombre à virgules) ?
Écrivont le test, qu’on rajoute à la suite, et regardons :
.F.
======================================================================
FAIL: test_add_with_float (__main__.TestChiffres)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_chiffres.py", line 17, in test_add_with_float
self.assertEqual(result, 3.011)
AssertionError: 3 != 3.011
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
La ligne qui nous renseigne ici est surtout la suivante :
Et effectivement int(a)+int(b) ne retournera pas de float, changeons le code pour que le test passe maintenant avec succès :
def add (a, b):
if type(a) == type('') or type(b) == type(''):
raise ValueError
return a+b
Il y a sûrement un meilleur moyen de vérifier que les arguments ne sont pas des chaines, mais pour l'exemple, ça fera l'affaire.
On exécute une dernière fois le test :
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
Voilà, grâce au test, on sait que la fonction à un comportement auquel on s’attend, on pourrait essayer de faire quelques modification, mais avec les tests, on s'assure que son comportement ne change pas de façon inattendu.
Note: Pour les floats et à cause du caractère parfois approximatif de leur
résultat, il est sûrement préférable d'utiliser assertAlmostEqual
au lieu de assertEqual
Précisions
Verbosité
On peut rajouter de la verbosité pour les tests, avec l'une des deux méthodes suivante:
- Directement en ligne de commande
test_add_string (test_chiffres.TestChiffres) ... ok
test_add_with_float (test_chiffres.TestChiffres) ... ok
test_addition (test_chiffres.TestChiffres) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
- Directement dans le code
En remplacent le unittest.main() comme suit :
suite = unittest.TestLoader().loadTestsFromTestCase(TestChiffres)
unittest.TextTestRunner(verbosity=2).run(suite)
Liste des assert
On a vu jusqu'ici assertEqual et assertRaises, il en existe bien d'autre, dont voici la liste.
- assertEqual
- assertNotEqual
- assertTrue
- assertFalse
- assertIs
- assertIsNot
- assertTruetIsNone
- assertIsNotNone
- assertIsNottIn
- assertNotIn
- assertIsInstance
- assertNotIsInstance
- assertRaises
- assertRaisesRegex
- assertWarns
- assertWarnsRegex
Attention cependant à la compatibilité entre les versions de Python, pas mal de nouveautés on été introduite dans Python2.7 par exemple, ou 3.2. Pour aller plus loin, il est préférable de se référer à la documentation officiel.
Discover
Ce module vient maintenant avec une nouvelle option, 'discover', permettant d'automatiser la recherche des différents fichier de test, je ne m'étendrais pas sur ce détail, et on verra pourquoi dans le prochain billet.
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Conclusion
Voilà, c'est tout pour ce premier billet consacré au test avec Python, bien des aspects du module unittest à été passé sous silence, cependant si le sujet vous intéresse, ou si vous êtes simplement curieux, la suite de cette série de 4 ou 5 billets consacré au test viendra rapidement. Vous pouvez vous abonné au flux RSS de mon blog, si ce n'est pas déjà fait, pour ne rien manquer.