Quelques rappels et généralités sur les tests, Python et le TDD

Cette page a pour but de s'assurer que le lecteur est bien au fait de ce que l'on entend par testing unitaire et d'intégration, sur sa mise en œuvre avec Python (unittest) et sur la démarche de TDD (test driven development).

Test unitaire vs. test d'intégration

Question récurrente s'il en est ! Et à laquelle on trouve finalement peu de réponses claires, tant chaque développeur a son point de vue sur cette affaire. Voici une vision personnelle, qui vaut ce qu'elle vaut, mais qui vous permettra de vous faire une idée simple :

  • Un test unitaire est un procédé destiné à s'assurer du bon fonctionnement d'une unité de programme, généralement de petite taille.
  • Un test d'intégration vise à s'assurer du bon fonctionnement de la mise en œuvre conjointe de plusieurs unités de programme.

Finalement, cette distinction est souvent une affaire de sémantique. Dans la vraie vie, les développeurs Django écrivent des tests. Unitaires, d'intégration… Tout ceci se retrouve généralement « mélangé » dans les modules de tests.

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.

TDD : Test driven development, développement dirigé par les tests

Le TDD (test driven development ou développement dirigé par les tests en français) est à la mode… tout le monde en parle, mais peu de développeurs l'utilisent réellement et efficacement.

Et pourtant, agilité rime avec TDD : quand on veut livrer souvent, vite et bien, l'une des meilleures stratégies est de se laisser guider par le contrat de service de nos composants : autrement dit, on commence par écrire les tests, on les regarde échouer lamentablement, puis on écrit le code et… on espère qu'ils n'échouent plus.

Qu'à cela ne tienne : dans la suite de ce cours, nous tâcherons de faire les choses correctement, et d'adopter cette approche aussi souvent que possible !

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.