Utilisation du framework unittest dans Python

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 (via tearDown).
  • 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 StringIO par import io as StringIO
  • Adapter les appels à StringIO() et decode()
  • Corriger la syntaxe d'impression (print() au lieu de print >>)
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/tearDown pour garantir l’indépendance.
  • Utilisez setUpClass pour des ressources coûteuses partagées.
  • Exploitez les décorateurs @skip pour 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 verbosity selon le contexte (CI, développement local, etc).

Étiquettes: unittest Python Testing Unit-Testing automation

Publié le 25 septembre à 07h26