Les tests unitaires avec unittest en Python#
En ingénierie logicielle professionnelle, écrire du code qui fonctionne aujourd'hui n'est pas suffisant : il faut garantir qu'il continuera de fonctionner demain lorsqu'un collègue ou vous-même apporterez des modifications, corrigerez un bogue ou ferez évoluer une fonctionnalité.
C'est le rôle fondamental des tests unitaires (Unit Tests) : vérifier de façon automatisée et reproductible que chaque unité élémentaire de votre programme (une fonction, une méthode, une classe) produit scrupuleusement le résultat attendu pour un jeu d'entrées déterminé.
1. La pyramide des tests et le pattern AAA#
Dans l'industrie du logiciel, les tests sont organisés selon une pyramide hiérarchique :
flowchart TD
A[Tests End-to-End / UI : Rares et lents] --> B[Tests d'Integration : Validation des composants]
B --> C[Tests Unitaires : Base massive, rapides et automatises]
Chaque test unitaire doit être indépendant des autres et respecter le pattern canonique AAA : 1. Arrange (Préparer) : instancier les objets et préparer les données d'entrée. 2. Act (Agir) : appeler la fonction ou la méthode à tester. 3. Assert (Vérifier) : valider que la valeur retournée correspond rigoureusement à l'attente théorique.
2. Le framework natif unittest#
Python intègre nativement le framework unittest (historiquement inspiré de JUnit en Java). Il repose sur trois conventions strictes :
1. Les fichiers de test commencent généralement par test_*.py.
2. Les classes de test héritent de la classe de base unittest.TestCase.
3. Les méthodes de test commencent obligatoirement par le préfixe test_ (sinon le moteur de test les ignorera silencieusement).
3. Les assertions indispensables de TestCase#
Au sein d'un test unitaire, on ne vérifie pas avec des print(), mais avec des méthodes d'assertion :
| Méthode d'assertion | Vérification effectuée |
|---|---|
self.assertEqual(a, b) |
\(a == b\) (égalité de valeur) |
self.assertNotEqual(a, b) |
\(a \ne b\) (différence) |
self.assertTrue(x) |
bool(x) is True |
self.assertFalse(x) |
bool(x) is False |
self.assertIsNone(x) |
\(x \text{ is None}\) |
self.assertIn(a, b) |
\(a \in b\) (appartenance) |
self.assertAlmostEqual(a, b, places=2) |
Égalité de flottants à \(N\) décimales près |
self.assertRaises(ExceptionType) |
Vérifie qu'une exception spécifique est bien levée |
4. Exemple complet : Test d'un module bancaire#
Le code métier à tester (banque.py)#
class CompteBancaire:
def __init__(self, titulaire: str, solde: float = 0.0):
if solde < 0:
raise ValueError("Le solde initial ne peut pas etre negatif.")
self.titulaire = titulaire
self.solde = solde
def deposer(self, montant: float) -> None:
if montant <= 0:
raise ValueError("Le montant depose doit etre strictement positif.")
self.solde += montant
def retirer(self, montant: float) -> None:
if montant <= 0:
raise ValueError("Le montant retire doit etre strictement positif.")
if montant > self.solde:
raise RuntimeError("Provision insuffisante.")
self.solde -= montant
La suite de tests unitaires (test_banque.py)#
import unittest
# from banque import CompteBancaire
class TestCompteBancaire(unittest.TestCase):
def setUp(self):
"""Fixture exécutée AVANT chaque méthode de test."""
self.compte = CompteBancaire("Alice", 100.0)
def tearDown(self):
"""Fixture exécutée APRÈS chaque méthode de test (nettoyage)."""
pass
def test_solde_initial(self):
"""Vérifie l'initialisation conforme du compte."""
self.assertEqual(self.compte.solde, 100.0)
self.assertEqual(self.compte.titulaire, "Alice")
def test_depot_valide(self):
"""Vérifie qu'un dépôt crédite correctement le solde."""
self.compte.deposer(50.0)
self.assertEqual(self.compte.solde, 150.0)
def test_depot_negatif_leve_erreur(self):
"""Vérifie qu'un dépôt négatif lève un ValueError."""
with self.assertRaises(ValueError):
self.compte.deposer(-20.0)
def test_retrait_valide(self):
"""Vérifie qu'un retrait valide débite le compte."""
self.compte.retirer(40.0)
self.assertEqual(self.compte.solde, 60.0)
def test_retrait_depassant_solde_leve_erreur(self):
"""Vérifie qu'un retrait au-delà du solde est bloqué."""
with self.assertRaises(RuntimeError):
self.compte.retirer(200.0)
if __name__ == "__main__":
# Exécute tous les tests de la classe
unittest.main()
5. Exécution des tests en ligne de commande#
Pour lancer vos tests depuis le terminal :
# 1. Exécuter un fichier de test spécifique
python test_banque.py
# 2. Découverte automatique de tous les tests du projet
python -m unittest discover -v
La sortie console vous indiquera le bilan d'exécution :
test_depot_negatif_leve_erreur (__main__.TestCompteBancaire) ... ok
test_depot_valide (__main__.TestCompteBancaire) ... ok
test_retrait_depassant_solde_leve_erreur (__main__.TestCompteBancaire) ... ok
test_retrait_valide (__main__.TestCompteBancaire) ... ok
test_solde_initial (__main__.TestCompteBancaire) ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.002s
OK
6. Défi d'application#
À l'aide de unittest, concevez une suite de tests unitaires complète pour une fonction calculer_tva(montant_ht: float, taux: float = 0.20) -> float. Validez le calcul nominal pour un montant de 100 €, le taux par défaut, et assurez-vous qu'un ValueError est levé si le montant ou le taux est strictement négatif.
Commentaires