
unittest est le framework de test intégré à la bibliothèque standard de Python. On y écrit des tests dans des classes héritant de unittest.TestCase, avec des méthodes d'assertion comme assertEqual. Son grand atout : rien à installer, il est livré avec Python. Ce guide part de zéro : structurer un projet, écrire et lancer un premier test, maîtriser les assertions, factoriser avec setUp/tearDown, appliquer le cycle TDD et éviter les pièges classiques.
Il s'adresse aux débutants comme à celles et ceux qui reprennent un projet utilisant unittest. Si vous démarrez un nouveau projet, pytest est souvent plus concis, mais unittest reste incontournable : il est partout, sans dépendance, et pytest sait d'ailleurs exécuter les tests unittest. Le code a été testé avec Python 3.12.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Structurer un projet avec un dossier
tests/dédié. - Écrire et lancer un test avec
TestCaseet la découverte automatique. - Utiliser les assertions essentielles (
assertEqual,assertRaises...). - Factoriser la préparation avec
setUpettearDown. - Appliquer le cycle TDD Red-Green-Refactor sur un cas concret.
- Éviter les pièges qui rendent les tests fragiles ou inutiles.
Codez votre premier test
Section intitulée « Codez votre premier test »Avant de plonger dans l'utilisation de unittest, la bonne nouvelle, c'est qu'il n'y a rien à installer ! En effet, unittest fait partie intégrante de la bibliothèque standard de Python. Tout ce dont vous avez besoin, c’est une installation fonctionnelle de Python. Maintenant, voyons comment organiser votre environnement pour bien démarrer.
Organiser vos fichiers et dossiers pour les tests
Section intitulée « Organiser vos fichiers et dossiers pour les tests »Une bonne organisation de vos fichiers est essentielle pour un projet maintenable. Voici une structure classique que j’aime utiliser :
Répertoiremon_projet/
Répertoiresrc/ # Contient le code source
- exemple.py # Exemple de fichier de code
Répertoiretests/ # Contient vos tests unitaires
- test_exemple.py # Tests pour exemple.py
- init .py # Indique que c'est un package Python
- requirements.txt # Optionnel, pour gérer les dépendances
Avec cette structure :
- Le dossier
src/contient votre code principal. - Le dossier
tests/est dédié à vos fichiers de tests. Chaque fichier de test correspond idéalement à un fichier dans le dossiersrc/.
Configurer votre environnement pour unittest
Section intitulée « Configurer votre environnement pour unittest »Créez un fichier de test pour vous assurer que tout fonctionne correctement.
Voici un exemple minimaliste dans tests/test_exemple.py :
import unittest
class TestSetup(unittest.TestCase): def test_environment(self): self.assertEqual(1 + 1, 2)
if __name__ == '__main__': unittest.main()Ensuite, lancez la découverte automatique des tests avec cette commande :
python -m unittest discover -s testsL'option discover parcourt le dossier tests/ et exécute tous les fichiers
test_*.py. Si tout est bien configuré, vous verrez une sortie similaire à
celle-ci :
.----------------------------------------------------------------------Ran 1 test in 0.001s
OKLes bases de unittest : assertions et structure
Section intitulée « Les bases de unittest : assertions et structure »Maintenant que votre environnement est prêt, il est temps de plonger dans les bases de unittest. Dans cette partie, nous allons découvrir les assertions, qui sont au cœur des tests et la manière de structurer vos fichiers pour que tout reste clair et efficace.
Comprendre les assertions
Section intitulée « Comprendre les assertions »Les assertions sont des commandes utilisées pour vérifier que le résultat attendu correspond bien au résultat obtenu. Si une assertion échoue, le test est marqué comme échoué. Voici quelques assertions courantes que vous utiliserez souvent :
-
assertEqual(a, b) : Vérifie que
aest égal àb. Exemple :self.assertEqual(2 + 2, 4) -
assertNotEqual(a, b) : Vérifie que
an'est pas égal àb. -
assertTrue(expr) : Vérifie que
exprest vrai. Exemple :self.assertTrue(3 > 2) -
assertFalse(expr) : Vérifie que
exprest faux. -
assertIs(a, b) : Vérifie que
aetbsont le même objet (par identité). -
assertIsNone(expr) : Vérifie que
exprestNone. -
assertRaises(exception, callable, *args, **kwargs) : Vérifie qu’une exception spécifique est levée lors de l’appel d’une fonction ou méthode.
Avec ces assertions, vous pouvez couvrir presque tous les cas possibles dans vos tests.
Préparer et nettoyer avec setUp et tearDown
Section intitulée « Préparer et nettoyer avec setUp et tearDown »Quand plusieurs tests d'une même classe ont besoin du même contexte (un
objet, une connexion, un fichier temporaire), on évite de le recréer dans chaque
méthode grâce à setUp et tearDown. La méthode setUp s'exécute
avant chaque test, tearDown après chaque test, ce qui garantit que
chacun part d'un état propre et indépendant.
import unittest
class TestPanier(unittest.TestCase): def setUp(self): self.panier = Panier() # avant chaque test
def tearDown(self): self.panier.vider() # après chaque test
def test_ajout(self): self.panier.ajouter("pomme") self.assertEqual(len(self.panier), 1)
def test_panier_vide(self): self.assertEqual(len(self.panier), 0)Chaque test reçoit un self.panier neuf : test_ajout ne peut pas
influencer test_panier_vide. C'est l'équivalent, côté unittest, des
fixtures de pytest.
Appliquer le cycle TDD avec unittest
Section intitulée « Appliquer le cycle TDD avec unittest »Le Test-Driven Development (TDD) est une approche méthodique où les tests guident le développement de votre code. Avec unittest, ce processus reste simple. La suite applique le cycle Red-Green-Refactor sur un exemple concret.
Le cycle TDD : Red-Green-Refactor
Section intitulée « Le cycle TDD : Red-Green-Refactor »-
Red : Écrire un test qui échoue Vous commencez par écrire un test pour une fonctionnalité que vous souhaitez implémenter. Comme le code n’existe pas encore (ou est incomplet), ce test échouera. Cet échec est essentiel pour vérifier que le test détecte bien les problèmes.
-
Green : Écrire juste assez de code pour réussir le test Ensuite, vous implémentez le minimum de code nécessaire pour que le test passe. L’idée ici n’est pas d’écrire du code parfait, mais de prouver que la fonctionnalité de base fonctionne.
-
Refactor : Améliorer le code sans casser le test Une fois que le test passe, vous pouvez améliorer votre code : le rendre plus lisible, optimiser les performances, ou éliminer les redondances. Les tests déjà écrits garantissent que votre refactorisation n'introduit pas de bugs.
Exemple pratique : Une fonction de calcul d’intérêts
Section intitulée « Exemple pratique : Une fonction de calcul d’intérêts »Prenons un cas simple : une fonction qui calcule des intérêts simples. Voici comment appliquer le TDD.
Étape 1 : Red - Écrire un test qui échoue
Section intitulée « Étape 1 : Red - Écrire un test qui échoue »Commençons par écrire un test pour une fonction appelée calculer_interets.
Cette fonction n'existe pas encore, mais nous définissons ce qu’elle est censée
faire :
import unittestfrom src.interets import calculer_interets # Importer la fonction à tester
class TestInterets(unittest.TestCase): def test_calculer_interets(self): resultat = calculer_interets(1000, 0.05, 2) self.assertEqual(resultat, 100) # 1000 * 0.05 * 2
if __name__ == '__main__': unittest.main()Créer le fichier du code en ne déclarant que la fonction :
def calculer_interets(capital, taux, duree): # L'implémentation initiale peut être minimale pour le cycle Green. pass # Notre fonction ne fait rienEn exécutant ce test, vous obtiendrez une erreur du type :
F======================================================================FAIL: test_calculer_interets (test_exemple.TestInterets.test_calculer_interets)----------------------------------------------------------------------Traceback (most recent call last): File "/home/bob/Projets/unittest/test_exemple.py", line 8, in test_calculer_interets self.assertEqual(resultat, 100) # 1000 * 0.05 * 2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^AssertionError: None != 100
----------------------------------------------------------------------Ran 1 test in 0.000s
FAILED (failures=1)Étape 2 : Green - Implémenter juste assez de code
Section intitulée « Étape 2 : Green - Implémenter juste assez de code »Créons maintenant une version minimale de la fonction pour faire passer le test :
def calculer_interets(capital, taux, duree): return capital * taux * dureeEn relançant le test, voici le résultat :
.----------------------------------------------------------------------Ran 1 test in 0.001s
OKLe test passe. Mission accomplie pour cette étape !
Étape 3 : Refactor - Améliorer le code
Section intitulée « Étape 3 : Refactor - Améliorer le code »Regardons s’il y a des améliorations possibles. Par exemple, nous pourrions ajouter une validation pour les entrées négatives ou des taux supérieurs à 1 (ce qui serait irréaliste pour un taux d'intérêt annuel).
Ajoutons un nouveau test pour vérifier ce comportement :
def test_valeurs_negatives(self): with self.assertRaises(ValueError): calculer_interets(-1000, 0.05, 2)En réexécutant tous les tests, vous validez que la refactorisation n’a pas cassé les fonctionnalités existantes.
.F======================================================================FAIL: test_valeurs_negatives (test_exemple.TestInterets.test_valeurs_negatives)----------------------------------------------------------------------Traceback (most recent call last): File "/home/bob/Projets/unittest/test_exemple.py", line 11, in test_valeurs_negatives with self.assertRaises(ValueError):AssertionError: ValueError not raised
----------------------------------------------------------------------Ran 2 tests in 0.000s
FAILED (failures=1)Codons notre fonction pour qu'elle lève le raise attendu :
def calculer_interets(capital, taux, duree): if capital < 0 or taux < 0 or duree < 0: raise ValueError("Les valeurs doivent être positives.") return capital * taux * dureeOn relance les tests :
..----------------------------------------------------------------------Ran 2 tests in 0.000s
OKLes deux tests passent : la fonction gère désormais le cas nominal et le cas d'erreur.
Les bénéfices du cycle TDD avec unittest
Section intitulée « Les bénéfices du cycle TDD avec unittest »- Développement guidé par les fonctionnalités : Vous écrivez du code en réponse à des besoins précis, ce qui évite de coder des fonctionnalités inutiles.
- Moins de bugs : Les tests vous alertent immédiatement si une modification casse une fonctionnalité existante.
- Code maintenable : Le processus de refactorisation garantit que votre code reste propre et facile à comprendre.
Un conseil pour bien démarrer
Section intitulée « Un conseil pour bien démarrer »Au début, il peut être tentant d’écrire une grande partie du code avant les tests. Résistez à cette tentation ! Respecter le cycle Red-Green-Refactor vous aidera à développer un code plus robuste, tout en vous habituant à penser aux tests comme une partie intégrante du développement.
Éviter les pièges courants avec unittest
Section intitulée « Éviter les pièges courants avec unittest »Même si unittest est un outil puissant, il est facile de tomber dans des pièges qui rendent vos tests moins efficaces, voire inutiles. Dans ce chapitre, nous allons examiner les erreurs courantes et les meilleures pratiques pour les éviter.
Écrire des tests trop dépendants du code
Section intitulée « Écrire des tests trop dépendants du code »Le piège : Vos tests sont si étroitement liés à l'implémentation actuelle qu'une petite modification (comme un changement de nom de méthode ou de structure) casse de nombreux tests. Cela rend leur maintenance difficile et décourage l'écriture de tests.
Comment l’éviter :
- Testez le comportement, pas l’implémentation. Concentrez-vous sur ce que le code doit faire et non sur comment il le fait.
- Si possible, utilisez des interfaces stables ou des fonctions publiques comme points d’entrée pour vos tests.
Exemple mauvais :
self.assertEqual(mon_objet._attribut_cache, 42) # Ne testez pas un attribut interne.Exemple correct :
self.assertEqual(mon_objet.get_valeur(), 42) # Testez la méthode publique.Ne pas tester les cas limites
Section intitulée « Ne pas tester les cas limites »Le piège : Vous testez uniquement les scénarios "heureux", c'est-à-dire les cas où tout fonctionne parfaitement, en oubliant les situations inhabituelles ou les erreurs possibles.
Comment l’éviter :
- Ajoutez des tests pour les entrées non valides, comme des valeurs nulles, des nombres négatifs, ou des chaînes vides.
- Simulez des erreurs système, comme une base de données inaccessible ou un fichier introuvable.
Exemples à tester :
- Que se passe-t-il si on passe
Noneà votre fonction ? - Votre fonction retourne-t-elle une exception appropriée pour un taux d’intérêt négatif ?
def test_valeurs_invalides(self): with self.assertRaises(ValueError): calculer_interets(-1000, 0.05, 2)Ignorer la lisibilité des tests
Section intitulée « Ignorer la lisibilité des tests »Le piège : Vos tests sont complexes, avec des noms de méthodes peu explicites ou des blocs de code durs à lire. Les tests deviennent aussi difficiles à comprendre que le code qu'ils testent.
Comment l’éviter :
- Donnez des noms explicites à vos tests pour indiquer clairement leur objectif.
- Utilisez des commentaires ou des docstrings pour expliquer des scénarios complexes.
Exemple mauvais :
def test_1(self): # Un nom de méthode inutile qui ne dit rien sur le test.Exemple correct :
def test_calcul_interets_valeurs_positives(self): """Test du calcul d'intérêts avec des valeurs positives standard.""" self.assertEqual(calculer_interets(1000, 0.05, 2), 100)Tester plusieurs choses dans un seul test
Section intitulée « Tester plusieurs choses dans un seul test »Le piège : Vous combinez plusieurs vérifications dans un seul test. Si une vérification échoue, il est difficile de savoir exactement où se situe le problème.
Comment l’éviter :
- Chaque test doit avoir un objectif unique. Divisez les tests si nécessaire.
- Utilisez des classes ou des fichiers séparés pour tester des fonctionnalités distinctes.
Exemple mauvais :
def test_calculations(self): self.assertEqual(addition(1, 2), 3) self.assertEqual(soustraction(5, 3), 2)Exemple correct :
def test_addition(self): self.assertEqual(addition(1, 2), 3)
def test_soustraction(self): self.assertEqual(soustraction(5, 3), 2)Ne pas exécuter les tests régulièrement
Section intitulée « Ne pas exécuter les tests régulièrement »Le piège : Vous écrivez des tests, mais vous les exécutez rarement, ou uniquement avant une mise en production. Cela augmente le risque de bugs.
Comment l’éviter :
- Exécutez les tests à chaque modification majeure du code.
- Intégrez les tests dans un pipeline CI/CD pour les exécuter automatiquement à chaque commit.
Exemple : Avec Gitlab CI,
configurez un fichier .gitlab-ci.yml pour automatiser vos tests :
stages: - test
test: stage: test script: - python -m unittest discover -s testsÀ retenir
Section intitulée « À retenir »- unittest est intégré à Python : aucune installation, on structure les tests en classes héritant de
TestCase. - On lance la suite avec
python -m unittest discover, qui trouve tous lestest_*.py. - Les assertions (
assertEqual,assertRaises,assertTrue...) sont le cœur des tests. setUp/tearDownpréparent et nettoient le contexte avant/après chaque test.- Le cycle TDD Red-Green-Refactor fait écrire le test avant le code.
- Pièges à éviter : tester l'implémentation, ignorer les cas limites, multiplier les vérifications dans un seul test.
- Depuis Python 3.12, les vieux alias (
assertEquals...) sont supprimés : utilisez les noms modernes.
FAQ : unittest en Python
Section intitulée « FAQ : unittest en Python »unittest.TestCase, avec des méthodes d'assertion :import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(2 + 2, 4)
Son avantage : aucune installation, il est livré avec Python. Sa contrepartie : une syntaxe plus verbeuse que pytest, qui reste le standard pour les nouveaux projets.# tests/test_calculs.py
import unittest
from src.calculs import addition
class TestCalculs(unittest.TestCase):
def test_addition(self):
self.assertEqual(addition(2, 3), 5)
python -m unittest discover -s tests
discover trouve et exécute tous les fichiers test_*.py. Vous pouvez aussi lancer un fichier précis avec python -m unittest tests.test_calculs.TestCase :| Assertion | Vérifie que |
|---|---|
assertEqual(a, b) |
a == b |
assertNotEqual(a, b) |
a != b |
assertTrue(x) / assertFalse(x) |
x est vrai / faux |
assertIs(a, b) / assertIsNone(x) |
identité / None |
assertIn(a, b) |
a est dans b |
assertRaises(Err) |
une exception est levée |
with self.assertRaises(ValueError):
int("abc")
Elles couvrent la grande majorité des cas.setUp et tearDown encadrent chaque test de la classe :class TestPanier(unittest.TestCase):
def setUp(self):
self.panier = Panier() # avant chaque test
def tearDown(self):
self.panier.vider() # après chaque test
def test_ajout(self):
self.panier.ajouter("pomme")
self.assertEqual(len(self.panier), 1)
setUp prépare un contexte commun, tearDown nettoie. Ils évitent la duplication et garantissent que chaque test part d'un état propre.- unittest : intégré, en classes, aucune dépendance. Utile dans un contexte qui l'impose déjà.
- pytest : à installer, en fonctions avec
assert, plus concis et messages d'échec détaillés.
TestCase. Du code ancien qui les utilise plante désormais avec AttributeError :| Alias supprimé | Nom moderne |
|---|---|
assertEquals |
assertEqual |
assertNotEquals |
assertNotEqual |
failUnless |
assertTrue |
failIf |
assertFalse |
assertRegexpMatches |
assertRegex |
assertRaisesRegexp |
assertRaisesRegex |
assertDictContainsSubset |
(supprimé) |
s final).Prochaines étapes
Section intitulée « Prochaines étapes »Les projets réels dépendent d'API et de bases de données. L'étape suivante consiste à isoler ces dépendances avec des mocks, ou à passer à pytest pour des tests plus concis.