
Un mock est un objet qui imite une dépendance réelle pour isoler le code testé. Quand une fonction appelle une API, une base de données ou lit un fichier, on ne veut pas que le test dépende du réseau ou d'un service externe : on remplace cette dépendance par un mock qui renvoie une valeur contrôlée. Ce guide couvre le module standard unittest.mock (Mock, MagicMock, patch), la fixture monkeypatch de pytest et le plugin pytest-mock, sur un cas concret : simuler un appel requests sans toucher au réseau. Tout le code a été exécuté avec Python 3.12 et pytest 9.1.
Il s'adresse aux personnes à l'aise avec pytest qui veulent des tests rapides, déterministes et capables de vérifier les cas d'erreur difficiles à provoquer en vrai.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre ce qu'est un mock et quand l'utiliser.
- Créer des faux objets avec
MocketMagicMock. - Remplacer une dépendance avec
patch, au bon endroit. - Utiliser
monkeypatchetpytest-mockdans vos tests. - Simuler une API et ses erreurs sans dépendre du réseau.
Pourquoi mocker une dépendance
Section intitulée « Pourquoi mocker une dépendance »Un bon test unitaire vérifie une seule unité de code, en isolation. Si votre fonction appelle une API météo, un test qui tape réellement l'API devient lent, fragile (il échoue si le réseau tombe) et non déterministe (la valeur change). Pire, certains cas sont presque impossibles à provoquer en vrai : un timeout, une erreur 500, une réponse malformée.
Le mock résout tout cela en remplaçant la dépendance par un objet que vous contrôlez. Vous décidez ce qu'il renvoie, vous simulez une panne à la demande, et le test s'exécute en millisecondes sans réseau. La règle : on mocke ce qui est hors du périmètre testé (appels externes, horloge, aléatoire), pas la logique que le test doit précisément valider.
Créer un faux objet avec Mock et MagicMock
Section intitulée « Créer un faux objet avec Mock et MagicMock »Le module standard unittest.mock fournit des objets qui acceptent n'importe quel appel et enregistrent comment ils ont été utilisés. Un MagicMock va plus loin que Mock en supportant aussi les méthodes spéciales (len(), itération, etc.).
from unittest.mock import MagicMock
service = MagicMock()service.get_utilisateur.return_value = {"nom": "Alice"}
resultat = service.get_utilisateur(42)
assert resultat == {"nom": "Alice"}service.get_utilisateur.assert_called_once_with(42)Deux attributs structurent tout mock :
return_value: ce que le mock renvoie quand on l'appelle.side_effect: une exception à lever ou une fonction à exécuter à la place, pour simuler une erreur ou un comportement dynamique.
Et des méthodes d'assertion vérifient les interactions : assert_called_once_with(...), assert_called(), assert_not_called(), plus l'attribut call_count.
Remplacer une dépendance avec patch
Section intitulée « Remplacer une dépendance avec patch »Créer un mock ne suffit pas : il faut l'injecter à la place du vrai objet. C'est le rôle de patch, qui remplace temporairement une cible pendant la durée du test, puis la restaure automatiquement. On l'utilise en décorateur ou en gestionnaire de contexte.
from unittest.mock import patch
@patch("mon_module.requests.get")def test_appel(mock_get): mock_get.return_value.json.return_value = {"temp": 21} # ... le test ...from unittest.mock import patch
def test_appel(): with patch("mon_module.requests.get") as mock_get: mock_get.return_value.json.return_value = {"temp": 21} # ... le test ...Cas concret : simuler un appel d'API
Section intitulée « Cas concret : simuler un appel d'API »Prenons un module qui interroge une API météo avec requests :
import requests
def temperature(ville): r = requests.get(f"https://api.exemple.com/meteo/{ville}", timeout=5) r.raise_for_status() return r.json()["temp"]On veut tester temperature() sans appeler l'API. On patche meteo.requests.get et on configure la réponse simulée :
from unittest.mock import patch
import meteo
@patch("meteo.requests.get")def test_temperature(mock_get): # la réponse simulée : r.json() renvoie {"temp": 21} mock_get.return_value.json.return_value = {"temp": 21} mock_get.return_value.raise_for_status.return_value = None
assert meteo.temperature("lyon") == 21
# on vérifie l'URL et le timeout réellement utilisés mock_get.assert_called_once_with( "https://api.exemple.com/meteo/lyon", timeout=5 )Le test valide la logique (construction de l'URL, extraction de temp) sans dépendre d'un service externe. Il s'exécute en quelques millisecondes et donne toujours le même résultat.
Simuler une erreur avec side_effect
Section intitulée « Simuler une erreur avec side_effect »Le vrai intérêt des mocks apparaît pour tester les pannes. Avec side_effect, le mock lève une exception au lieu de renvoyer une valeur, ce qui simule un réseau coupé sans avoir à débrancher quoi que ce soit :
import pytest
import meteo
@patch("meteo.requests.get", side_effect=ConnectionError("réseau indisponible"))def test_temperature_reseau_coupe(mock_get): with pytest.raises(ConnectionError): meteo.temperature("lyon")Vous vérifiez ainsi que votre code réagit correctement à une erreur impossible à déclencher de façon fiable en conditions réelles.
monkeypatch et pytest-mock
Section intitulée « monkeypatch et pytest-mock »Si vous écrivez vos tests avec pytest, deux alternatives à patch s'offrent à vous, souvent plus lisibles.
La fixture monkeypatch est intégrée à pytest et remplace un attribut le temps du test, sans décorateur ni import :
def test_temperature(monkeypatch): faux_reponse = MagicMock() faux_reponse.json.return_value = {"temp": 5} monkeypatch.setattr(meteo.requests, "get", lambda *a, **k: faux_reponse)
assert meteo.temperature("paris") == 5Le plugin pytest-mock ajoute la fixture mocker, qui enveloppe unittest.mock et nettoie automatiquement les patches à la fin du test :
def test_temperature(mocker): mock_get = mocker.patch("meteo.requests.get") mock_get.return_value.json.return_value = {"temp": 30}
assert meteo.temperature("nice") == 30Pièges à éviter
Section intitulée « Pièges à éviter »Le mocking est puissant, mais mal dosé il produit des tests trompeurs.
- Patcher au mauvais endroit : toujours la cible là où elle est utilisée (voir l'avertissement plus haut). C'est de loin l'erreur la plus courante.
- Sur-mocker : si vous mockez la logique que le test devrait vérifier, le test ne teste plus rien. Ne mockez que les dépendances externes.
- Mock trop permissif : un
MagicMockaccepte n'importe quel appel, même une méthode qui n'existe pas sur le vrai objet. Utilisezautospec=True(patch(..., autospec=True)) pour que le mock respecte la vraie signature et attrape les fautes de frappe. - Oublier de vérifier les interactions : un mock qui renvoie la bonne valeur ne prouve pas que votre code l'a appelé correctement. Ajoutez un
assert_called_once_with(...)quand l'appel compte.
À retenir
Section intitulée « À retenir »- Un mock remplace une dépendance réelle pour rendre un test rapide, déterministe et isolé.
MagicMockcrée un faux objet ;return_valuefixe ce qu'il renvoie,side_effectsimule une erreur.patchinjecte le mock à la place du vrai objet, en décorateur ou en context manager.- On patche là où l'objet est utilisé (
mon_module.requests.get), jamais là où il est défini. monkeypatch(pytest) etmocker(pytest-mock) sont des alternatives plus lisibles àpatch.side_effectpermet de tester les pannes impossibles à provoquer en vrai.- Ne mockez que les dépendances externes et pensez à
autospec=Truepour des mocks fidèles.
FAQ : les mocks en Python
Section intitulée « FAQ : les mocks en Python »- il renvoie une valeur fixée (
return_value), - il enregistre comment il a été appelé (pour vérifier les interactions),
- il peut simuler une erreur (
side_effect).
Mock: n'implémente pas les dunder methods (__len__,__iter__,__enter__...).MagicMock: les supporte, donc gèrelen(), l'itération, les comparaisons, lewith.
from unittest.mock import MagicMock
service = MagicMock()
service.get.return_value = {"ok": True}
En pratique, on utilise MagicMock par défaut, et c'est d'ailleurs ce que patch fournit automatiquement.patch remplace une cible par un mock le temps du test, puis la restaure. En décorateur ou en context manager :from unittest.mock import patch
@patch("meteo.requests.get")
def test_appel(mock_get):
mock_get.return_value.json.return_value = {"temp": 21}
assert meteo.temperature("lyon") == 21
Le mock est injecté en paramètre du test. Point crucial : la cible désigne l'endroit où l'objet est utilisé (meteo.requests.get), pas où il est défini.patch remplace une référence dans un espace de noms précis, pas l'objet partout à la fois.Si meteo.py fait import requests puis appelle requests.get(...), la référence à remplacer est meteo.requests.get :@patch("meteo.requests.get") # correct
# @patch("requests.get") # le vrai code s'exécute quand même
Patcher requests.get globalement n'affecte pas la référence déjà importée dans meteo. Résultat : le test tape réellement le réseau sans que vous le voyiez.| Outil | Origine | Usage |
|---|---|---|
unittest.mock |
standard | patch, Mock, en décorateur |
monkeypatch |
pytest natif | remplacer un attribut simple |
mocker (pytest-mock) |
plugin | unittest.mock + nettoyage auto |
# pytest-mock : le plus agréable avec pytest
def test_x(mocker):
mocker.patch("meteo.requests.get")
monkeypatch ne demande aucune dépendance ; mocker est le plus pratique dès qu'on utilise pytest.side_effect avec l'exception à lever :import pytest
from unittest.mock import patch
@patch("meteo.requests.get", side_effect=ConnectionError("réseau coupé"))
def test_panne(mock_get):
with pytest.raises(ConnectionError):
meteo.temperature("lyon")
À l'appel, le mock lève l'exception au lieu de renvoyer une valeur. Cela simule une panne réseau ou une erreur serveur impossible à provoquer de façon fiable en vrai. side_effect accepte aussi une fonction pour un comportement dynamique.Prochaines étapes
Section intitulée « Prochaines étapes »Le mocking s'appuie sur un framework de test et sert surtout à isoler les appels externes comme les API.