Comprendre les tests unitaires : exemples en Pythton

Cette page vous présentera quelques techniques de testing unitaire avec le langage Python. Au delà d'une implémentation de xUnit, Python propose un procédé de test original, doctest, permettant d'inclure des tests dans la documentation technique.

Python et les tests

Bien qu'il ne constitue pas la seule alternative possible, Python vient avec un module de testing assez complet et pratique nommé unittest.

unittest permet de définir des cas de tests, avec notamment un jeu d'assertions assez complet et pratique.

Tester en Python avec unittest

Un exemple vaut mieux que mille mots : voyons comment utiliser unittest sur un cas simple.

Code
import unittest

def is_even(nbr):
    """
    Cette fonction teste si un nombre est pair.
    """
    return nbr % 2 == 0

class MyTest(unittest.TestCase):
    def test_is_even(self):
        self.assertTrue(is_even(2))
        self.assertFalse(is_even(1))
        self.assertEqual(is_even(0), True)

if __name__ == '__main__':
    unittest.main()
Exécution
.
--------------------
Ran 1 test in 0.001s

OK

Comme le montre cet exemple, un test case (qui est en fait plutôt, au fond, un use case qui peut regrouper plusieurs test cases…) est une classe qui hérite de la classe TestCase définie dans le module unittest que nous avons importé pour l'occasion.

Une classe de test (TestCase) est composée de méthodes définissant chacune un test. Chacune de ces méthodes définit à son tour différentes assertions, ou vérifications. Notre classe MyTest comporte donc un « test », qui réalise 3 vérifications.

Bien entendu, en Python comme dans beaucoup d'autres langages, on définit généralement les classes de test dans des fichiers différents des classes métiers ou des scripts eux-mêmes. L'exemple présenté ci-dessus regroupe le code et les tests dans le même fichier pour des besoins de simplification.

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

Python possède un outil très appréciable, souple, pratique et agréable pour inclure des tests simples dans des méthodes ou fonctions sans avoir à définir de classes de test particulières. Cet outil standard s'appelle doctest.

Grâce à doctest, le programmeur peut inclure des tests unitaires directement dans la documentation des fonctions et méthodes !

Ici encore, voyons comment cela fonctionne sur un exemple : reprenons notre exemple précédent en intégrant les tests directement dans la documentation de la fonction.

Code
def is_even(nbr):
    """
    Cette fonction teste si un nombre est pair.
    >>> is_even(2)
    True
    >>> is_even(1)
    False
    >>> is_even(0)
    True
    """
    return nbr % 2 == 0

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

Et voilà ! Notre fonction simple est testée ! À l'exécution, rien n'est affiché : normal, il n'y a pas d'erreur… Introduisons une coquille pour voir ce qui se passera :

Code
def is_even(nbr):
    """
    Cette fonction teste si un nombre est pair.
    >>> is_even(2)
    True
    >>> is_even(1)
    False
    >>> is_even(0)
    True
    """
    return nbr % 2 == 1 # <= Ce code est buggé !

if __name__ == '__main__':
    import doctest
    doctest.testmod()
Exécution
**********************************
File "./petit_pepaire.py", line 7, in __main__.is_even
Failed example:
    is_even(2)
Expected:
    True
Got:
    False
**********************************
File "./petit_pepaire.py", line 9, in __main__.is_even
Failed example:
    is_even(1)
Expected:
    False
Got:
    True
**********************************
File "./petit_pepaire.py", line 11, in __main__.is_even
Failed example:
    is_even(0)
Expected:
    True
Got:
    False
**********************************
1 items had failures:
   3 of   3 in __main__.is_even
***Test Failed*** 3 failures.

Un exemple plus complet…

Nous allons à présent montrer l'utilisation d'une classe de test pour implémenter des tests unitaires.

Soit une fonction Python permettant de nettoyer un texte pour le faire respecter un maximum les règles de typographie française :

Code applicatif
def clean_text(text):
    """
    Nettoyage d'une pavé de texte pour respecter la typographie française.
    """
    ret = re.sub(r'^(\s*)(.*?)(\s*)$', r'\2', text) # Suppression des espaces au début et à la fin du texte
    ret = re.sub(r'\s*\!{2,}', u'!', ret)
    ret = re.sub(r'\s*\?{2,}', u'?', ret)
    ret = re.sub(r"\s*\,+", u',', ret)
    ret = re.sub(r'\,([^\s])', r', \1', ret)
    ret = re.sub(r'\s*\.{3,}', u'…', ret) # Remplacement de "(espace)...(x fois)" par "…"
    ret = re.sub(r'\s\.', u'.', ret) # Remplacement de " ." par "."
    ret = re.sub(r'\.{2}', u'…', ret) # Remplacement de ".." par "…"
    ret = re.sub(r'([^0-9])\1{3,}', r'\1', ret) # Suppression des multiples répétitions de caractères (plus de 3) sauf pour les chiffres
    ret = re.sub(r'([^\s])([?!:])', r'\1 \2', ret) # Remplacement de "blabla!" par "blabla !"
    ret = re.sub(r'([?!])([^\s:\)])', r'\1 \2', ret) # Remplacement de "?blabla" par "? blabla"
    return ret
Code de test
import unittest

class TestText(unittest.TestCase):
    def setUp(self):
        self.text_checks = (
            (u'Un texte avec pluuuuuuusieurs', u'Un texte avec plusieurs'),
            (u'Collage!', u'Collage !'),
            (u'!Collage', u'! Collage'),
            (u'Espaces      en trop', u'Espaces en trop'),
            (u"Trop d'exclamation!!!! ou d'interrogation ??", u"Trop d\'exclamation ! ou d\'interrogation ?"),
            (u'   Espaces debut ou fin ', u'Espaces debut ou fin'),
            (u'Smileys :), ;):)', u'Smileys :), ;) :)'),
            (u'(Une ponctuation avant une parenthese !)', u'(Une ponctuation avant une parenthese !)'),
            (u'Quelques paquerettes . .', u'Quelques paquerettes…'),
            (u'[color=red][Edit : merci de respecter les regles et de proposer des titres explicites.][/color]', u'[color=red][Edit : merci de respecter les regles et de proposer des titres explicites.][/color]'),
            (u'60000 ans', u'60000 ans'),
            (u'Une , virgule', u'Une, virgule'),
            (u'Deux,, virgules', u'Deux, virgules'),
            (u'Virgule,collée', u'Virgule, collée'),
        )

    def test_clean_text(self):
        for check in self.text_checks:
            self.assertEqual(clean_text(check[0]), check[1])

Testez vos connaissances

Est-ce que les tests unitaires sont plus importants que les tests d'intégration ?
  • Oui
  • Non, c'est comme se demander si l'écran est plus important que le disque dur : les deux sont importants.
Quel est l'intérêt des doctests en Python ?
  • Il permettent de définir plus de tests à la fois.
  • C'est une méthode très simple pour définir rapidement et de façon commode quelques tests d'une méthode ou d'une fonction.
  • Le module doctest fait partie des modules de base de Python, ce qui est un avantage par rapport à unittest.
Où peut-on définir des classes de test d'une classe C définie dans un fichier F en Python ?
  • Dans F
  • Dans un autre fichier que F
À quoi sert la méthode setUp() d'une classe de test case unittest Python ?
  • Cette méthode est dépréciée aujourd'hui, et n'a plus d'utilité.
  • À initialiser les variables qui seront utiles dans les différents tests de la classe de test.
  • Elle permet de libérer la mémoire après l'exécution des tests.

Soit le code suivant. Que se passe-t-il quand on exécute le script ?

import unittest

class Ginette:
	def dire_bonjour(self, nom):
		return "Bonjour {nom}, je suis Ginette".format(nom=nom)

class GinetteTest(unittest.TestCase):
	def setUp(self):
		self.ginou = Ginette()

	def test_bonjour_quelquun(self):
		self.assertEqual(self.ginou.dire_bonjour('Simone'), "Bonjour Berthe, je suis Ginette")

if __name__ == '__main__':
    unittest.main()
  • Rien, le script n'est pas valide.
  • On obtient un rapport de test positif qui dit que tout s'est bien passé.
  • Un seul test est exécuté.
  • Deux tests sont exécutés.
  • Trois tests sont exécutés.
  • On obtient un rapport de test négatif.