Tests · Qualité

Tests Python

Écrire des tests fiables avec unittest et pytest. Mock, TDD, fixtures, coverage — les outils du développeur professionnel.

🐍 Python · Tests Unitaires · Guide Complet

Écrire du code
auquel on peut faire confiance

Un guide exhaustif des tests unitaires en Python — de la théorie aux outils, mocks avancés inclus. Conçu pour les bacheliers en informatique qui maîtrisent déjà Python.

01 — Fondamentaux

Pourquoi tester son code ?

Un test unitaire est un morceau de code qui vérifie qu'une petite unité de ton programme — une fonction, une méthode — se comporte exactement comme prévu. C'est le filet de sécurité de tout développeur sérieux.

🛡️ Prévenir les régressions

Chaque modification est validée par les tests. Refactoriser sans peur, c'est possible dès que la couverture est bonne.

📝 Documenter le comportement

Un test est une spécification exécutable — plus fiable qu'un commentaire qui peut devenir obsolète.

🏗️ Améliorer le design

Du code difficile à tester est souvent mal conçu. Tester pousse à écrire des fonctions simples et découplées.

🔄 CI/CD & automatisation

Les tests s'exécutent à chaque push. Un échec bloque le déploiement — gage de qualité en continu.

💡

Le principe AAA : Chaque test suit — Arrange (préparer), Act (appeler), Assert (vérifier). C'est la structure universelle d'un test lisible et maintenable.

02 — Bibliothèque Standard

Le module unittest

Inclus dans la bibliothèque standard Python, unittest s'inspire de JUnit (Java). Disponible partout sans installation. Verbeux mais structuré et complet.

Structure d'un test

test_calculatrice.py
import unittest

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

class TestDiviser(unittest.TestCase):

    def setUp(self):
        """Appelé avant CHAQUE méthode de test"""
        self.numerateur = 10

    def test_division_normale(self):
        resultat = diviser(self.numerateur, 2)       # Act
        self.assertEqual(resultat, 5.0)               # Assert

    def test_division_par_zero(self):
        with self.assertRaises(ValueError):
            diviser(self.numerateur, 0)

    def test_division_float(self):
        self.assertAlmostEqual(diviser(1, 3), 0.333, places=2)

if __name__ == "__main__":
    unittest.main()

Cycle de vie d'un test

setUpClassUne fois/classe
setUpAvant chaque test
test_xxxLe test
tearDownAprès chaque test
tearDownClassUne fois/classe

Toutes les méthodes assert*

assertEqual(a, b)
a == b
assertNotEqual(a, b)
a != b
assertTrue(x)
bool(x) est True
assertFalse(x)
bool(x) est False
assertIs(a, b)
a is b (identité objet)
assertIsNone(x)
x is None
assertIn(a, b)
a in b
assertIsInstance(a, b)
isinstance(a, b)
assertRaises(Exc)
Lève l'exception Exc
assertAlmostEqual(a,b)
Flottants ≈ égaux
assertGreater(a, b)
a > b
assertRegex(s, r)
regex r trouvé dans s

Sous-tests avec subTest

subtest.py
class TestMultiples(unittest.TestCase):
    def test_est_pair(self):
        cas = [(2, True), (3, False), (0, True), (-4, True)]
        for n, attendu in cas:
            with self.subTest(n=n):
                # Si ce subTest échoue, les autres continuent quand même
                self.assertEqual(n % 2 == 0, attendu)

Tester une classe POO avec unittest

En programmation orientée objet, on teste chaque méthode publique de la classe, ainsi que les interactions entre objets. La règle d'or : un test = un comportement, pas une méthode entière.

🎯

Ce qu'on teste en POO : la construction de l'objet (__init__), chaque méthode publique isolément, les changements d'état internes, la levée d'exceptions métier, et les interactions entre plusieurs objets.

banque.py — la classe à tester
# banque.py ───────────────────────────────────────
class CompteBancaire:
    """Compte bancaire simple avec solde, dépôts et retraits."""

    def __init__(self, titulaire: str, solde_initial: float = 0.0):
        if solde_initial < 0:
            raise ValueError("Le solde initial ne peut pas être négatif")
        self.titulaire = titulaire
        self._solde = solde_initial
        self._historique = []

    @property
    def solde(self) -> float:
        return self._solde

    def deposer(self, montant: float) -> None:
        if montant <= 0:
            raise ValueError("Le montant doit être positif")
        self._solde += montant
        self._historique.append(("dépôt", montant))

    def retirer(self, montant: float) -> None:
        if montant <= 0:
            raise ValueError("Le montant doit être positif")
        if montant > self._solde:
            raise RuntimeError(f"Solde insuffisant : {self._solde:.2f}€")
        self._solde -= montant
        self._historique.append(("retrait", montant))

    def virement(self, destinataire: "CompteBancaire", montant: float) -> None:
        self.retirer(montant)          # peut lever RuntimeError
        destinataire.deposer(montant)

    def nb_operations(self) -> int:
        return len(self._historique)

    def __repr__(self) -> str:
        return f"Compte({self.titulaire!r}, solde={self._solde:.2f}€)"
test_banque_unittest.py
import unittest
from banque import CompteBancaire

class TestCompteBancaire(unittest.TestCase):

    # ── setUp : état propre avant chaque test ─────────
    def setUp(self):
        """Crée deux comptes réutilisables dans tous les tests."""
        self.alice = CompteBancaire("Alice", solde_initial=500.0)
        self.bob   = CompteBancaire("Bob",   solde_initial=200.0)

    # ── Tests du constructeur ─────────────────────────
    def test_init_solde_correct(self):
        compte = CompteBancaire("Charlie", 100.0)
        self.assertEqual(compte.solde, 100.0)
        self.assertEqual(compte.titulaire, "Charlie")

    def test_init_solde_zero_par_defaut(self):
        compte = CompteBancaire("Dave")
        self.assertEqual(compte.solde, 0.0)

    def test_init_solde_negatif_interdit(self):
        with self.assertRaises(ValueError):
            CompteBancaire("Eve", solde_initial=-10)

    # ── Tests de dépôt ───────────────────────────────
    def test_depot_augmente_solde(self):
        self.alice.deposer(100.0)
        self.assertEqual(self.alice.solde, 600.0)

    def test_depot_enregistre_historique(self):
        self.alice.deposer(50.0)
        self.assertEqual(self.alice.nb_operations(), 1)

    def test_depot_montant_zero_interdit(self):
        with self.assertRaises(ValueError):
            self.alice.deposer(0)

    def test_depot_montant_negatif_interdit(self):
        with self.assertRaises(ValueError):
            self.alice.deposer(-50)

    # ── Tests de retrait ─────────────────────────────
    def test_retrait_diminue_solde(self):
        self.alice.retirer(200.0)
        self.assertEqual(self.alice.solde, 300.0)

    def test_retrait_solde_insuffisant(self):
        with self.assertRaisesRegex(RuntimeError, "Solde insuffisant"):
            self.alice.retirer(600.0)   # alice n'a que 500€

    def test_retrait_solde_reste_inchange_si_echec(self):
        # Le solde ne doit PAS changer si le retrait échoue
        try:
            self.alice.retirer(9999)
        except RuntimeError:
            pass
        self.assertEqual(self.alice.solde, 500.0)   # inchangé !

    # ── Tests de virement (interaction 2 objets) ─────
    def test_virement_debit_emetteur(self):
        self.alice.virement(self.bob, 100.0)
        self.assertEqual(self.alice.solde, 400.0)

    def test_virement_credit_destinataire(self):
        self.alice.virement(self.bob, 100.0)
        self.assertEqual(self.bob.solde, 300.0)

    def test_virement_insuffisant_aucun_changement(self):
        # Si le virement échoue, les DEUX comptes restent intacts
        try:
            self.alice.virement(self.bob, 9999)
        except RuntimeError:
            pass
        self.assertEqual(self.alice.solde, 500.0)
        self.assertEqual(self.bob.solde, 200.0)

    # ── Test de l'état interne après séquence ────────
    def test_sequence_operations(self):
        self.alice.deposer(100)
        self.alice.retirer(50)
        self.alice.deposer(200)
        self.assertEqual(self.alice.solde, 750.0)      # 500+100-50+200
        self.assertEqual(self.alice.nb_operations(), 3)  # 3 opérations

    # ── Test de représentation ───────────────────────
    def test_repr(self):
        self.assertIn("Alice", repr(self.alice))
        self.assertIn("500.00", repr(self.alice))

Bonne pratique : Chaque méthode de test ne doit tester qu'une seule chose. Remarque que test_virement_debit_emetteur et test_virement_credit_destinataire sont séparés, même s'ils testent le même appel — l'un vérifie Alice, l'autre vérifie Bob.

03 — L'outil Moderne

Tester avec pytest

pytest est le standard de facto dans l'industrie Python. Pas de classes obligatoires, assertions naturelles, plugins puissants, sortie lisible. Plus expressif et plus rapide à écrire.

📦

Installation : pip install pytest — puis pytest pour lancer tous les tests découverts automatiquement.

🐌 Avec unittest

import unittest
class TestAdd(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(add(2,3), 5)

⚡ Avec pytest

# Pas de classe, pas d'import !

def test_addition():
    assert add(2, 3) == 5

Parametrize — plusieurs cas élégamment

test_param.py
import pytest

@pytest.mark.parametrize("entree, attendu", [
    (2,  True),
    (3,  False),
    (0,  True),
    (-4, True),
])
def test_est_pair(entree, attendu):
    assert est_pair(entree) == attendu  # 4 tests générés

# pytest.raises avec vérification du message
def test_exception():
    with pytest.raises(ValueError, match=r"zéro"):
        diviser(10, 0)

Fixtures avec scopes et teardown

conftest.py
import pytest

@pytest.fixture                        # scope="function" par défaut
def utilisateur():
    return {"nom": "Alice", "score": 0}

@pytest.fixture
def db(tmp_path):
    conn = init_db(tmp_path / "test.db")
    yield conn        # ← setUp / tearDown en un seul bloc
    conn.close()

@pytest.fixture(scope="module")     # une fois par fichier
def connexion_api():
    return creer_connexion_test()

# Injection automatique par nom
def test_insertion(db, utilisateur):
    db.inserer(utilisateur)
    assert db.count() == 1

Markers — organiser les tests

test_markers.py
@pytest.mark.skip(reason="pas encore implémenté")
def test_future(): pass

@pytest.mark.xfail(reason="bug connu #123")
def test_bug_connu():
    assert calcul_bugge() == 42

@pytest.mark.slow
def test_long():
    # $ pytest -m "not slow"  →  exclut ce test
    pass

Tester une classe POO avec pytest

Avec pytest, les fixtures remplacent avantageusement le setUp. On peut grouper les tests d'une classe dans une class Test… sans hériter de TestCase — ce qui garde tous les avantages de pytest.

test_banque_pytest.py
import pytest
from banque import CompteBancaire

# ── Fixtures partagées ───────────────────────────
@pytest.fixture
def alice():
    return CompteBancaire("Alice", solde_initial=500.0)

@pytest.fixture
def bob():
    return CompteBancaire("Bob", solde_initial=200.0)

# ── Classe de tests groupés (sans héritage) ──────
class TestConstructeur:
    def test_solde_initial(self):
        c = CompteBancaire("Charlie", 100.0)
        assert c.solde == 100.0
        assert c.titulaire == "Charlie"

    def test_solde_zero_par_defaut(self):
        assert CompteBancaire("Dave").solde == 0.0

    def test_solde_negatif_interdit(self):
        with pytest.raises(ValueError):
            CompteBancaire("Eve", solde_initial=-10)

class TestDepot:
    def test_augmente_solde(self, alice):
        alice.deposer(100.0)
        assert alice.solde == 600.0

    def test_enregistre_historique(self, alice):
        alice.deposer(50.0)
        assert alice.nb_operations() == 1

    @pytest.mark.parametrize("montant_invalide", [0, -1, -100])
    def test_montant_invalide(self, alice, montant_invalide):
        with pytest.raises(ValueError):
            alice.deposer(montant_invalide)

class TestRetrait:
    def test_diminue_solde(self, alice):
        alice.retirer(200.0)
        assert alice.solde == 300.0

    def test_solde_insuffisant(self, alice):
        with pytest.raises(RuntimeError, match="Solde insuffisant"):
            alice.retirer(600.0)

    def test_solde_inchange_si_echec(self, alice):
        with pytest.raises(RuntimeError):
            alice.retirer(9999)
        assert alice.solde == 500.0  # inchangé

class TestVirement:
    def test_debit_emetteur(self, alice, bob):
        alice.virement(bob, 100.0)
        assert alice.solde == 400.0

    def test_credit_destinataire(self, alice, bob):
        alice.virement(bob, 100.0)
        assert bob.solde == 300.0

    def test_echec_aucun_changement(self, alice, bob):
        with pytest.raises(RuntimeError):
            alice.virement(bob, 9999)
        assert alice.solde == 500.0
        assert bob.solde   == 200.0

# ── Test de séquence d'état ───────────────────────
def test_sequence_operations(alice):
    alice.deposer(100)
    alice.retirer(50)
    alice.deposer(200)
    assert alice.solde == 750.0
    assert alice.nb_operations() == 3
🔍

Avantage des fixtures pytest pour la POO : chaque test reçoit un objet fraîchement instancié — pas de risque qu'un test modifie l'état et affecte le suivant. Avec setUp en unittest, ce risque existe si on oublie de réinitialiser un attribut.

Comparatif

Critèreunittestpytest
Installation✅ Standard Python📦 pip install pytest
SyntaxeClasses + self.assertX()Fonctions + assert natif
FixturessetUp/tearDown@pytest.fixture (puissant)
ParametrizesubTest (limité)@pytest.mark.parametrize
Messages d'erreurBasiquesDétaillés avec diff intelligente
PluginsPeuÉcosystème riche (cov, mock…)
Compatible unittest✅ Runner les classes TestCase
🎭 Section Approfondie

Les Mocks — Maîtrise totale

Un mock est un objet factice qui imite une dépendance réelle (API, DB, fichier, GPIO...). Il permet de tester en isolation totale, de contrôler les effets de bord et de simuler des conditions difficiles à reproduire en vrai.

04 — Mock & Isolation Avancée

Maîtriser unittest.mock

Cette section couvre les fondations, les mocks chaînés, le mocking de fichiers, les outils avancés (ANY, sentinel, call_args) et toutes les stratégies de patch pour isoler n'importe quelle dépendance.

Mock vs MagicMock — choisir le bon outil

🔷 Mock

Objet générique qui accepte tout. N'implémente pas les méthodes spéciales Python (__len__, __iter__...). À préférer quand tu contrôles tout manuellement.

MagicMock

Sous-classe de Mock qui implémente automatiquement les méthodes dunder. Utilisé par défaut dans patch(). Idéal pour les context managers, iterables, etc.

mock_vs_magic.py
from unittest.mock import Mock, MagicMock

m = Mock()
mm = MagicMock()

# Mock basique — configure le retour
m.return_value = 42
assert m() == 42

# Mock — les méthodes spéciales échouent
try:
    len(m)    # → TypeError : Mock ne supporte pas __len__
except TypeError:
    pass

# MagicMock — les méthodes spéciales fonctionnent
mm.__len__.return_value = 5
assert len(mm) == 5          # ✅

mm.__iter__.return_value = iter([1, 2, 3])
assert list(mm) == [1, 2, 3]  # ✅

# Context manager — seul MagicMock fonctionne naturellement
with mm as ctx:
    ctx.lire()   # ✅ pas d'erreur

# spec= : limite aux attributs d'une vraie classe
from datetime import datetime
m_safe = MagicMock(spec=datetime)
m_safe.year           # ✅ datetime a .year
# m_safe.inexistant  # → AttributeError (protège les fautes de frappe)

Les trois formes de patch()

⚠️

Règle d'or : Toujours patcher là où l'objet est utilisé, pas là où il est défini. Si mon_module.py fait from os import path, tu patches mon_module.path, pas os.path.

trois_formes_patch.py
from unittest.mock import patch, MagicMock

# ── 1. Context manager ────────────────────────────
def test_context_manager():
    with patch("app.requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"prix": 42.5}
        resultat = recuperer_prix("BTC")
        assert resultat == 42.5
    # Hors du bloc : requests.get est restauré automatiquement

# ── 2. Décorateur — injecté en paramètre (ordre inversé !) ──
@patch("app.smtplib.SMTP")
@patch("app.requests.get")
def test_decorateur(mock_get, mock_smtp):
    # ATTENTION : le plus bas décorateur = premier paramètre
    mock_get.return_value.status_code = 200
    envoyer_rapport()
    mock_smtp.return_value.sendmail.assert_called_once()

# ── 3. Fixture pytest (recommandé avec pytest-mock) ──────
def test_fixture(mocker):
    mock_db = mocker.patch("app.db.connexion")
    mock_db.return_value.query.return_value = ["alice", "bob"]
    assert lister_utilisateurs() == ["alice", "bob"]
    # Nettoyage automatique après le test, sans with ni décorateur

Mocks chaînés — appels en cascade

Quand le code enchaîne des appels (obj.attr.methode().resultat), un Mock crée automatiquement des enfants. Comprendre cette hiérarchie est essentiel pour configurer les bons retours.

Hiérarchie générée automatiquement par Mock :

mock_requests
→ .get(url)
mock_requests.get
→ retourne
mock_requests.get.return_value
↓ .json()
mock_requests.get.return_value.json
→ retourne
mock_requests.get.return_value.json.return_value
mocks_chaines.py
from unittest.mock import patch, MagicMock, call

# ── Exemple : code qui chaîne des appels ──────────
# def recuperer_temperature(ville):
#     resp = requests.get(url)
#     return resp.json()["main"]["temp"]

def test_chaine_simple():
    with patch("meteo.requests.get") as mock_get:
        # Configurer la chaîne complète :
        mock_get.return_value.json.return_value = {"main": {"temp": 22.5}}

        temp = recuperer_temperature("Paris")
        assert temp == 22.5

        # Vérifier la chaîne d'appels
        mock_get.assert_called_once_with("https://api.meteo.io/Paris")
        mock_get.return_value.json.assert_called_once()

# ── Mocks chaînés plus profonds ───────────────────
# Code : db.session.query(User).filter_by(id=1).first()
def test_chaine_orm():
    with patch("app.db") as mock_db:
        mock_user = MagicMock(nom="Alice", age=30)
        mock_db.session.query.return_value.filter_by.return_value.first.return_value = mock_user

        user = trouver_utilisateur(id=1)
        assert user.nom == "Alice"

# ── Plusieurs appels successifs (side_effect liste) ─
def test_appels_successifs():
    with patch("app.lire_capteur") as mock_cap:
        # Simule 3 lectures consécutives du capteur
        mock_cap.side_effect = [23.1, 23.4, 23.2]

        valeurs = [lire_capteur() for _ in range(3)]
        assert sum(valeurs) / 3 == pytest.approx(23.23, abs=0.01)

# ── Nested patches : plusieurs patches simultanés ─
@patch("app.envoyer_sms")
@patch("app.envoyer_email")
@patch("app.db.sauvegarder")
def test_notification_complete(mock_save, mock_email, mock_sms):
    # Ordre : bas vers haut dans les décorateurs
    notifier_utilisateur("Alice", "Votre commande est prête")

    mock_save.assert_called_once()
    mock_email.assert_called_once_with("Alice", "Votre commande est prête")
    mock_sms.assert_called_once()

Mocker les fichiers et l'I/O

Tester du code qui lit ou écrit des fichiers sans créer de vrais fichiers sur le disque — essentiel pour des tests rapides, isolés et reproductibles.

test_io.py
from unittest.mock import patch, mock_open, MagicMock

# ── Le code à tester ──────────────────────────────
# def lire_config(chemin):
#     with open(chemin, "r") as f:
#         return json.load(f)

# ── 1. mock_open : simuler open() ─────────────────
def test_lire_config():
    contenu_json = '{"host": "localhost", "port": 5432}'
    m = mock_open(read_data=contenu_json)

    with patch("builtins.open", m):
        config = lire_config("config.json")

    assert config["host"] == "localhost"
    assert config["port"] == 5432
    m.assert_called_once_with("config.json", "r")

# ── 2. mock_open en écriture ──────────────────────
# def sauvegarder_log(message, chemin):
#     with open(chemin, "a") as f:
#         f.write(f"{datetime.now()} - {message}\n")
def test_sauvegarder_log():
    m = mock_open()
    with patch("builtins.open", m), patch("mon_module.datetime") as mock_dt:
        mock_dt.now.return_value.__str__.return_value = "2024-01-01 10:00:00"
        sauvegarder_log("Démarrage", "app.log")

    m.assert_called_with("app.log", "a")
    # Vérifier ce qui a été écrit
    handle = m.return_value.__enter__.return_value
    handle.write.assert_called_once_with("2024-01-01 10:00:00 - Démarrage\n")

# ── 3. tmp_path (pytest) : vraie alternative propre ──
def test_avec_tmp_path(tmp_path):
    # pytest crée un vrai répertoire temporaire → supprimé après le test
    fichier = tmp_path / "config.json"
    fichier.write_text('{"host": "db", "port": 5432}')

    config = lire_config(str(fichier))
    assert config["port"] == 5432

# ── 4. Mocker os.path et os.listdir ───────────────
def test_lister_fichiers_log():
    with patch("os.listdir", return_value=["app.log", "error.log", "data.csv"]), \
         patch("os.path.isfile", return_value=True):

        logs = lister_fichiers_log("/var/log")
        assert len(logs) == 2  # seulement les .log
        assert "data.csv" not in logs

Outils avancés : ANY, sentinel, call_args

outils_avances.py
from unittest.mock import Mock, MagicMock, call, ANY, sentinel, patch

# ════════════════════════════════════════════════
# ANY — correspond à n'importe quelle valeur
# ════════════════════════════════════════════════
def test_any():
    m = Mock()
    m("Alice", age=30, timestamp=1704067200)

    # On veut vérifier le nom mais peu importe les autres args
    m.assert_called_once_with("Alice", age=ANY, timestamp=ANY)

    # ANY est égal à tout
    assert ANY == 42
    assert ANY == "hello"
    assert ANY == None
    assert ANY == [1, 2, 3]

def test_any_dans_liste():
    m = Mock()
    m(1); m(2); m(3)
    # Vérifier uniquement le premier et dernier appel
    assert m.call_args_list == [call(1), call(ANY), call(3)]

# ════════════════════════════════════════════════
# sentinel — créer des valeurs uniques et nommées
# ════════════════════════════════════════════════
def test_sentinel():
    # sentinel.NOM crée un objet unique (singleton)
    VIDE = sentinel.VIDE
    NON_INIT = sentinel.NON_INITIALISE

    # Utile pour distinguer "None" de "pas de valeur"
    def get_valeur(cle, defaut=sentinel.ABSENT):
        if defaut is sentinel.ABSENT:
            raise KeyError(f"Clé '{cle}' introuvable")
        return defaut

    assert get_valeur("x", None) is None  # None est une valeur explicite
    with pytest.raises(KeyError):
        get_valeur("x")              # sans défaut → exception

# ════════════════════════════════════════════════
# call_args — inspecter les arguments d'appel
# ════════════════════════════════════════════════
def test_call_args():
    m = Mock()
    m(10, 20, label="test", actif=True)

    # call_args = (args, kwargs) du dernier appel
    args, kwargs = m.call_args

    assert args == (10, 20)
    assert kwargs["label"] == "test"
    assert kwargs["actif"] is True

def test_call_args_list():
    m = Mock()
    m("a", x=1)
    m("b", x=2)
    m("c")

    # Inspecter chaque appel individuellement
    premier_appel = m.call_args_list[0]
    assert premier_appel == call("a", x=1)

    # Extraire et tester un paramètre spécifique de chaque appel
    tous_x = [c.kwargs["x"] for c in m.call_args_list if "x" in c.kwargs]
    assert tous_x == [1, 2]

# ════════════════════════════════════════════════
# call() — construire des appels attendus
# ════════════════════════════════════════════════
def test_call_object():
    m = Mock()
    m(1, 2)
    m("hello", key="val")

    # Comparer la séquence complète d'appels
    assert m.call_args_list == [
        call(1, 2),
        call("hello", key="val"),
    ]

    # assert_has_calls : l'ordre peut être flexible
    m.assert_has_calls([call(1, 2), call("hello", key="val")])
    m.assert_has_calls([call("hello", key="val")], any_order=True)

side_effect — comportements dynamiques

side_effect.py
from unittest.mock import Mock

m = Mock()

# 1. Exception → levée à l'appel
m.side_effect = ConnectionError("Réseau indisponible")

# 2. Liste → valeurs retournées une par une
m.side_effect = [23.1, 23.4, 23.2]
# m() → 23.1,  m() → 23.4,  m() → 23.2,  m() → StopIteration

# 3. Mélange valeurs/exceptions
m.side_effect = [10, IOError("Capteur déconnecté"), 12]
# 1er appel → 10, 2ème → lève IOError, 3ème → 12

# 4. Fonction → logique conditionnelle complète
def simuler_capteur(pin):
    if pin == 4:
        return 36.6   # capteur température
    elif pin == 17:
        return 0      # GPIO LOW
    raise ValueError(f"Pin {pin} inconnu")

m.side_effect = simuler_capteur
assert m(4) == 36.6
assert m(17) == 0

autospec et create_autospec — mocks sûrs

autospec.py
from unittest.mock import create_autospec, patch

class CapteurI2C:
    def __init__(self, adresse: int):
        self.adresse = adresse

    def lire_temperature(self) -> float: ...
    def calibrer(self, offset: float, unite: str = "C") -> None: ...

# create_autospec : vérifie la signature à chaque appel
mock_capteur = create_autospec(CapteurI2C, instance=True)
mock_capteur.lire_temperature.return_value = 22.5

mock_capteur.lire_temperature()          # ✅ OK
# mock_capteur.lire_temperature(42)      # → TypeError : trop d'args
mock_capteur.calibrer(0.5, unite="F")  # ✅ OK
# mock_capteur.calibrer(0.5, "F", "X")  # → TypeError
# mock_capteur.inexistant               # → AttributeError

# Avec patch + autospec=True (recommandé en prod)
@patch("app.CapteurI2C", autospec=True)
def test_lecture_securisee(mock_cls):
    instance = mock_cls.return_value
    instance.lire_temperature.return_value = 21.0

    systeme = SystemeAlerte()
    systeme.verifier_temperature()
    instance.lire_temperature.assert_called_once()

Récapitulatif — arsenal complet

OutilUsage
Mock()Objet générique — tout accepté
MagicMock()Mock + méthodes dunder (__len__, __iter__…)
patch("module.obj")Remplace temporairement un objet dans un module
patch.object(cls, "attr")Patche un attribut d'une classe spécifique
mock_open(read_data=…)Simule open() pour les tests de fichiers
return_valueCe que retourne le mock quand appelé
side_effectException, liste de retours, ou fonction
ANYPlaceholder qui matche n'importe quelle valeur
sentinel.NOMObjet unique nommé (pour distinguer None d'"absent")
call_args / call_args_listInspecter les arguments des appels passés
call()Construire un appel attendu pour comparaison
create_autospec(cls)Mock qui vérifie la signature à chaque appel
assert_called_with()Vérifie le dernier appel
assert_called_once_with()Vérifie qu'un seul appel avec ces args a eu lieu
assert_has_calls([])Vérifie une séquence d'appels (ordre optionnel)
assert_not_called()Vérifie que le mock n'a jamais été appelé
reset_mock()Efface l'historique des appels
🔌

Pour tester du matériel électronique (capteurs, GPIO, I2C, UART…), les mocks sont indispensables : on ne peut pas brûler un capteur ou dépendre d'un Raspberry Pi dans un pipeline CI. La page dédiée couvre tous ces cas.


Voir la page Électronique & Capteurs →