Prépa Python

Expert — chapitre 47 sur 54

Tests unitaires : unittest et pytest

Écrire des tests qui protègent votre code

60 minAbonnés

Comment garantir que votre code marche ? Et surtout : comment garantir qu'il continue à marcher après chaque modification ? La réponse professionnelle : les tests unitaires. Python offre deux outils : unittest (bibliothèque standard) et pytest (tierce, dominant dans la pratique moderne). Les tests sont attendus du niveau Expert et constituent un skill pro essentiel.

Pourquoi tester ?

Sans tests :

  • Chaque modification peut casser du code existant sans qu'on le sache.
  • Les bugs découverts en production coûtent 100 à 1000× plus cher qu'en développement.
  • On devient prudent avec son propre code, on évite de le modifier — il « rouille ».

Avec des tests :

  • Une modification qui casse quelque chose est détectée immédiatement.
  • On refactorise sans peur.
  • La suite de tests devient une documentation vivante du comportement attendu.
  • Les bugs trouvés deviennent des tests : on ne les reproduit plus.

Terminologie

  • Test unitaire : teste une unité (une fonction, une méthode) de façon isolée.
  • Test d'intégration : teste plusieurs composants ensemble.
  • Test de bout en bout (E2E) : teste l'application complète.

Ce chapitre couvre les tests unitaires.

Tester à la main vs tester avec un framework

À la main (avec assert)

python
def additionner(a, b):
    return a + b

# Test manuel
assert additionner(2, 3) == 5
assert additionner(0, 0) == 0
assert additionner(-1, 1) == 0

print("Tous les tests OK !")

C'est un début, mais limité :

  • Dès qu'un assert échoue, tout s'arrête.
  • Pas de rapport détaillé.
  • Difficile de tester les exceptions.
  • Pas de découverte automatique des tests.

Les frameworks résolvent tout ça.

unittest — le framework standard

unittest est dans la bibliothèque standard. Inspiré de JUnit (Java), il utilise une approche orientée objet.

Structure de base

python
import unittest

def additionner(a, b):
    return a + b

class TestAdditionner(unittest.TestCase):
    def test_positifs(self):
        self.assertEqual(additionner(2, 3), 5)

    def test_negatifs(self):
        self.assertEqual(additionner(-2, -3), -5)

    def test_zero(self):
        self.assertEqual(additionner(0, 5), 5)

# Lancement dans un script : unittest.main()
# Dans cette page, on lance la suite à la main :
suite = unittest.TestLoader().loadTestsFromTestCase(TestAdditionner)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

Anatomie

  • Une classe qui hérite de unittest.TestCase.
  • Chaque méthode commence par test_ — c'est le framework qui les détecte.
  • On utilise des méthodes self.assert* pour vérifier.

Les principales assertions

Méthode Vérifie
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNone(x) x is None
assertIn(a, b) a in b
assertIsInstance(x, cls) isinstance(x, cls)
assertRaises(exc) Lève l'exception attendue
assertAlmostEqual(a, b) Égalité approchée (flottants)

Tester une exception

python
import unittest

def diviser(a, b):
    if b == 0:
        raise ZeroDivisionError("Division par zéro")
    return a / b

class TestDiviser(unittest.TestCase):
    def test_division_normale(self):
        self.assertEqual(diviser(10, 2), 5)

    def test_division_par_zero(self):
        with self.assertRaises(ZeroDivisionError):
            diviser(10, 0)

    def test_message_erreur(self):
        with self.assertRaises(ZeroDivisionError) as ctx:
            diviser(10, 0)
        self.assertIn("Division par zéro", str(ctx.exception))

suite = unittest.TestLoader().loadTestsFromTestCase(TestDiviser)
unittest.TextTestRunner(verbosity=2).run(suite)

setUp et tearDown

Méthodes appelées avant/après chaque test — pour préparer un état et nettoyer.

python
import unittest

class CompteBancaire:
    def __init__(self, solde=0):
        self.solde = solde

    def deposer(self, montant):
        self.solde += montant

    def retirer(self, montant):
        if montant > self.solde:
            raise ValueError("Fonds insuffisants")
        self.solde -= montant

class TestCompteBancaire(unittest.TestCase):
    def setUp(self):
        """Appelé AVANT chaque test."""
        self.compte = CompteBancaire(100)

    def test_depot(self):
        self.compte.deposer(50)
        self.assertEqual(self.compte.solde, 150)

    def test_retrait(self):
        self.compte.retirer(30)
        self.assertEqual(self.compte.solde, 70)

    def test_retrait_excessif(self):
        with self.assertRaises(ValueError):
            self.compte.retirer(200)

suite = unittest.TestLoader().loadTestsFromTestCase(TestCompteBancaire)
unittest.TextTestRunner(verbosity=2).run(suite)

setUp est rejoué avant chaque test : on a toujours un compte neuf avec 100 €. Impossible qu'un test en pollue un autre.

pytest — le framework moderne

pytest est une bibliothèque tierce (pip install pytest) qui est devenue le standard de facto dans le monde Python. Plus concis, plus puissant, plus Pythonique que unittest.

Syntaxe minimaliste

python
# fichier : test_calcul.py

def additionner(a, b):
    return a + b

def test_positifs():
    assert additionner(2, 3) == 5

def test_negatifs():
    assert additionner(-2, -3) == -5

Lancer les tests :

bash
pytest                         # exécute tous les test_*.py du dossier
pytest -v                      # verbeux
pytest test_calcul.py          # un fichier précis
pytest test_calcul.py::test_positifs   # un test précis

Avantages de pytest

  • Juste des fonctions avec assert — pas de classe à écrire.
  • Messages d'erreur détaillés qui montrent les valeurs.
  • Pas d'assertEqual etc. — le bon vieux assert suffit.
  • Fixtures (voir plus bas) plus puissantes que setUp.
  • Plugins nombreux (coverage, parallelisme, etc.).

Tester les exceptions

python
import pytest

def diviser(a, b):
    if b == 0:
        raise ZeroDivisionError("Division par zéro")
    return a / b

def test_division_par_zero():
    with pytest.raises(ZeroDivisionError):
        diviser(10, 0)

def test_message_exception():
    with pytest.raises(ZeroDivisionError, match="Division par zéro"):
        diviser(10, 0)

Le match= vérifie en plus que le message contient le texte donné (regex).

Fixtures

Une fixture est une fonction qui prépare un contexte pour les tests. On la déclare avec @pytest.fixture.

python
import pytest

class CompteBancaire:
    def __init__(self, solde=0):
        self.solde = solde

    def deposer(self, montant):
        self.solde += montant

@pytest.fixture
def compte():
    return CompteBancaire(100)

def test_depot(compte):
    compte.deposer(50)
    assert compte.solde == 150

def test_solde_initial(compte):
    assert compte.solde == 100

pytest injecte la fixture dans chaque test qui en déclare le nom en paramètre. Magique et puissant.

Paramétriser les tests

Pour tester plusieurs cas avec la même logique :

python
import pytest

def carre(x):
    return x ** 2

@pytest.mark.parametrize("entree, attendu", [
    (0, 0),
    (1, 1),
    (2, 4),
    (3, 9),
    (-4, 16),
])
def test_carre(entree, attendu):
    assert carre(entree) == attendu

pytest lance le test 5 fois, avec les paires (entrée, attendu). Rapport détaillé cas par cas.

Suite réservée aux abonnés

Débloquez l'intégralité du parcours

Les 54 chapitres, les exercices corrigés automatiquement, les examens blancs et le suivi de votre progression.

Voir les offresChapitres en accès libre

Déjà un accès ? Connectez-vous.