Introduction à unittest
Le module unittest fait partie de la bibliothèque standard de Python et fournit un cadre complet pour écrire et exécuter des tests unitaires. Inspiré de frameworks comme JUnit, il supporte l'organisation des tests, l'initialisation et le nettoyage d'environnements, ainsi que la gestion fine des résultats.
Concepts fondamentaux
- Test fixture : Ensemble d'objets initialisés avant un test (via
setUp) et nettoyés après (viatearDown). - Test case : Une instance de
unittest.TestCase, contenant une ou plusieurs méthodes de test. - Test suite : Groupe de cas de test ou de suites imbriquées.
- Test runner : Composant qui exécute les tests et produit un rapport (ex:
TextTestRunner).
Écriture de cas de test
Chaque méthode de test doit commencer par test_. Les classes héritent de TestCase.
import unittest
class CalculatorTest(unittest.TestCase):
def setUp(self):
self.calculator = Calculator()
def tearDown(self):
del self.calculator
def test_addition_positive(self):
result = self.calculator.add(3, 5)
self.assertEqual(result, 8)
def test_subtraction(self):
result = self.calculator.subtract(10, 4)
self.assertTrue(result == 6)
L’ordre d’exécution suit l’ordre alphabétique des noms de méthodes.
Gestion des fixtures au niveau classe et module
Pour exécuter une opération une seule fois par classe, utilisez des méthodes de classe :
@classmethod
def setUpClass(cls):
cls.database = connect_to_test_db()
@classmethod
def tearDownClass(cls):
cls.database.disconnect()
Ces méthodes sont appelées respectivement avant et après tous les tests de la classe.
Sauter des tests et gérer les échecs attendus
Il est possible de contrôler dynamiquement l'exécution des tests grâce aux décorateurs suivants :
class IntegrationTests(unittest.TestCase):
@unittest.skip("En cours de refactoring")
def test_legacy_feature(self):
pass
@unittest.skipIf(sys.version_info < (3, 8), "Nécessite Python 3.8+")
def test_modern_syntax(self):
pass
@unittest.skipUnless(os.getenv("TEST_EXTERNAL"), "Variable d'environnement non définie")
def test_api_call(self):
pass
@unittest.expectedFailure
def test_known_buggy_feature(self):
self.assertTrue(buggy_function()) # Échoue, mais ce n'est pas comptabilisé comme un échec
Assertions disponibles
TestCase propose de nombreuses méthodes d’assertion :
| Méthode | Vérifie |
|---|---|
assertEqual(a, b) |
a == b |
assertIn(item, container) |
item in container |
assertTrue(expr) |
bool(expr) is True |
assertIsNone(value) |
value is None |
assertRaises(Exception, func) |
La fonction lève une exception |
Organisation avec TestSuite
Regrouper des tests pour un contrôle plus fin :
def create_test_suite():
suite = unittest.TestSuite()
suite.addTest(CalculatorTest('test_addition_positive'))
suite.addTest(CalculatorTest('test_subtraction'))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner(verbosity=2)
runner.run(create_test_suite())
Exécution via unittest.main
La façon la plus simple d’exécuter des tests :
if __name__ == '__main__':
unittest.main(verbosity=2)
L'argument verbosity permet de choisir le niveau de détail : 0 (minimal), 1 (normal), 2 (détaillé).
Redirection des résultats vers un fichier
Pour enregistrer les résultats dans un fichier texte :
with open('results.txt', 'w') as f:
runner = unittest.TextTestRunner(stream=f, verbosity=2)
runner.run(create_test_suite())
Génération de rapports HTML
Bien que HTMLTestRunner ne soit plus maintenu officielelment, il reste utilisable avec quelques correctifs pour Python 3 :
- Remplacer
import StringIOparimport io as StringIO - Adapter les appels à
StringIO()etdecode() - Corriger la syntaxe d'impression (
print()au lieu deprint >>)
from HTMLTestRunner import HTMLTestRunner
with open('report.html', 'w') as f:
runner = HTMLTestRunner(
stream=f,
title='Rapport de test',
description='Tests fonctionnels automatisés',
verbosity=2
)
runner.run(create_test_suite())
Résumé des bonnes pratiques
- Structurez vos tests avec
setUp/tearDownpour garantir l’indépendance. - Utilisez
setUpClasspour des ressources coûteuses partagées. - Exploitez les décorateurs
@skippour désactiver conditionnellement des tests. - Préférez
unittest.main()pour une exécution simple depuis la ligne de commande. - Redirigez la sortie vers un fichier pour archiver les résultats.
- Ajustez
verbosityselon le contexte (CI, développement local, etc).