Les tests unitaires automatisés avec unittest¶
Un test automatisé est un programme qui se découpe en trois étapes dites AAA pour Arrange, Act, Assert.
- Arrange
La mise en place de l’environnement : création et initialisation des objets nécessaires à l’exécution du test.
- Act
Le test proprement dit.
- Assert
La vérification des résultats obtenus par le test.
Le sous-système (l’ensemble des objets) éprouvé par le test est parfois appelé SUT (System Under Test).
On distingue différentes catégories de tests :
Tests unitaires : testent une partie (une unité) d’un système afin de s’assurer qu’il fonctionne correctement (build the system right)
Tests d’acceptation : testent le système afin de s’assurer qu’il est conforme aux besoins (build the right system)
Tests d’intégration : testent le système sur une plate-forme proche de la plate-forme cible
Tests de sécurité : testent que l’application ne contient pas de failles de sécurité connues (injection de code, attaque XSS, …)
Tests de robustesse : testent le comportement de l’application au limite des ressources disponibles (mémoire, CPU, …) sur la plate-forme
Pour réaliser des tests unitaires, unittest
est le framework de test
intégré dans la bibliothèque standard Python.
Structure d’une classe de test¶
Les tests sont regroupés dans des classes de test. Généralement, on groupe dans
une classe les tests ayant la même classe ou le même module comme point d’entrée.
La classe de test doit impérativement héritée de unittest.TestCase
.
Les tests sont représentés par des méthodes dont le nom commence par test
.
import unittest
class UneClasseDeTest(unittest.TestCase):
def test_simple(self):
self.assertTrue(True)
Pour exécuter une classe de test, il faut utiliser la fonction
unittest.main()
. Pour permettre de rendre le module de test exécutable,
on ajoute donc dans le fichier :
if __name__ == '__main__':
unittest.main()
Il est possible d’exécuter des instructions avant et après chaque test pour
allouer et désallouer des ressources nécessaires à l’exécution des
tests. On redéfinit pour cela les méthodes setUp()
et tearDown()
.
import unittest
class UneClasseDeTest(unittest.TestCase):
def setUp(self):
print("Avant le test")
def tearDown(self):
print("Après le test")
def test_simple(self):
self.assertTrue(True)
if __name__ == '__main__':
unittest.main()
Pour réaliser les assertions, une classe de test hérite de
unittest.TestCase
des méthodes d’assertion.
Méthode |
vérifie |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Toutes ces méthodes acceptent le paramètre optionnel msg
pour passer un message
d’erreur à afficher si l’assertion échoue.
Exécution des tests¶
En rendant chaque module de test exécutable, il suffit de lancer le module lui-même pour produire un rapport de test.
import unittest
class ChaineDeCaractereTest(unittest.TestCase):
def test_reversed(self):
resultat = reversed("abcd")
self.assertEqual("dcba", "".join(resultat))
def test_sorted(self):
resultat = sorted("dbca")
self.assertEqual(['a', 'b', 'c', 'd'], resultat)
def test_upper(self):
resultat = "hello".upper()
self.assertEqual("HELLO", resultat)
if __name__ == '__main__':
unittest.main()
$ python3 test_str.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Il est également possible de lancer directement le module unittest
grâce
à l’option -m
en passant comme paramètre les modules qui définissent les
tests (le nom des fichiers sans l’extension .py
). On peut ainsi lancer
plusieurs modules de test comme une suite :
$ python3 -m unittest test_str
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Exercice - Tests unitaires de abs()
Écrire les tests unitaires pour la fonction abs()
.
Note
L’exercice précédent propose de tester une méthode sans effet de bord (ce que l’on appelle également une fonction pure). Les tests sur ce type de méthodes sont faciles à écrire. Ils restent cependant l’exception lorsqu’on utilise la programmation orientée objet. En effet, l’appel d’une méthode sur un objet modifie le plus souvent son état et provoque généralement des effets de bord en sollicitant d’autres objets avec lesquels l’objet entretient des dépendances.
Tester des exceptions¶
Les tests unitaires automatisés sont utiles pour vérifier qu’une fonction (ou une méthode) produit le résultat attendu si l’appel est correct mais ils sont également très utiles pour vérifier qu’elle produit l’erreur attendue dans certains cas.
Pour tester qu’une exception survient, on peut utiliser la méthode
TestCase.assertRaises
conjointement
avec une structure with
.
import unittest
class AbsTest(unittest.TestCase):
def test_abs_n_accepte_pas_une_chaine_de_caracteres(self):
with self.assertRaises(TypeError):
abs("a")
if __name__ == '__main__':
unittest.main()
$ python3 test_abs.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Utilisation de doublure¶
Parfois, il est utile de contrôler l’environnement de test d’un objet ou d’une collaboration d’objets. Pour cela, on peut faire appel à des doublures qui vont se substituer lors des tests aux objets réellement utilisés lors de l’exécution de l’application dans un environnement de production.
- Simulateur
Un simulateur fournit une implémentation alternative d’un sous-système. Un simulateur remplace un sous-système qui n’est pas disponible pour l’environnement de test. Par exemple, on peut remplacer un système de base de données par une implémentation simplifiée en mémoire.
- Fake object
Un fake object permet de remplacer un sous-système dont il est difficile de garantir le comportement. Le comportement du fake object est défini par le test et est donc déterministe. Par exemple, si un objet dépend des informations retournées par un service Web, il est souhaitable de remplacer pour les tests l’implémentation du client par une implémentation qui retournera une réponse déterminée par le test lui-même.
- Mock object
Un objet mock est proche d’un fake object sauf qu’un objet mock est également capable de faire des assertions sur les méthodes qui sont appelées et les paramètres qui sont transmis à ces méthodes.
Utilisation d’un objet mock¶
Le module unittest.mock
permet de créer facilement des objets mock.
Ce module fournit les classes Mock
et
MagicMock
(cette dernière est une extension
Mock
pour le support automatique des méthodes spéciales
ou dunders). On peut programmer le comportement des méthodes d’un objet mock
et vérifier ensuite qu’il a bien été utilisé comme attendu au cours du test.
Supposons que nous voulions tester la fonction suivante :
from pathlib import Path
def is_sourcefile(path):
"""Retourne True si le fichier est un fichier source Python"""
if not path.is_file():
raise Exception("Fichier indisponible")
return path.suffix == ".py"
Nous pouvons utiliser un mock à la place du paramètre path
pour contrôler
l’appel de la méthode is_file()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import unittest
from unittest.mock import Mock
class FonctionTest(unittest.TestCase):
def test_is_sourcefile_when_sourcefile(self):
path = Mock()
path.is_file.return_value = True
path.suffix = ".py"
resultat = is_sourcefile(path)
self.assertTrue(resultat)
path.is_file.assert_called()
def test_is_sourcefile_when_file_does_not_exist(self):
path = Mock()
path.is_file.return_value = False
with self.assertRaises(Exception):
is_sourcefile(path)
path.is_file.assert_called()
def test_is_sourcefile_when_not_expected_suffix(self):
path = Mock()
path.is_file.return_value = True
path.suffix = ".txt"
resultat = is_sourcefile(path)
self.assertFalse(resultat)
path.is_file.assert_called()
if __name__ == '__main__':
unittest.main()
|
À la ligne 8, nous créons un mock et nous pouvons prédéfinir la valeur qui sera
retournée par la méthode is_file()
ainsi que la valeur
de l’attribut suffix
.
Puis à la ligne 15, nous contrôlons que la méthode is_file()
a bien était appelée pendant l’exécution du test.