Retour au cours

backend / python

Tests avec pytest : fixtures et mocks

Leçon 191 exercice

Explication

Ce que vous allez apprendre

  • Écrire un test pytest lisible avec une simple fonction test_... et un assert
  • Tester plusieurs cas avec @pytest.mark.parametrize sans dupliquer le code de test
  • Utiliser une fixture pour préparer et nettoyer un contexte de test automatiquement
  • Isoler le code testé de ses dépendances externes avec unittest.mock.Mock et patch
  • Vérifier qu'une dépendance mockée a bien été appelée avec assert_called_once_with

Dans quel contexte ?

Une pull request échoue en intégration continue avec le message AssertionError: assert 21 == 20 sur un test test_calcul_remise. Sans ce test automatisé, ce bug de calcul de remise serait passé inaperçu jusqu'à ce qu'un client se plaigne d'une facture incorrecte. Un test qui échoue en CI avant la mise en production, plutôt qu'un bug découvert par un utilisateur, est exactement la raison d'être des tests automatisés.

Pourquoi on ne vérifie plus son code à l'œil

Relancer manuellement son programme après chaque modification pour vérifier que rien n'est cassé devient vite intenable dès que le projet grossit. Un test automatisé encode cette vérification une fois pour toutes : on l'écrit, et on peut le relancer en une seconde autant de fois que nécessaire, à chaque modification future, sans jamais oublier un cas qu'on aurait vérifié manuellement autrefois.

pytest, ou tester sans cérémonie

Contrairement à d'autres frameworks de test plus verbeux, pytest permet d'écrire un test comme une simple fonction commençant par test_, avec un assert classique. Pas besoin d'hériter d'une classe spéciale ni d'apprendre une API de comparaison dédiée : c'est l'une des raisons de son adoption massive dans l'écosystème Python.

BesoinOutil pytest
Vérifier un résultatassert resultat == attendu
Vérifier qu'une exception est levéewith pytest.raises(ValueError):
Tester plusieurs jeux de données@pytest.mark.parametrize(...)
Préparer/nettoyer un contexte@pytest.fixture avec yield
Isoler une dépendance externeunittest.mock.patch

Fixtures : préparer un contexte sans le répéter

Beaucoup de tests ont besoin du même objet préparé en amont (une instance, une connexion simulée). Plutôt que de dupliquer ce code de préparation dans chaque test, une fixture le centralise : pytest l'exécute automatiquement et l'injecte dans chaque test qui la déclare en paramètre. Le yield à l'intérieur d'une fixture sépare la préparation (avant) du nettoyage (après), un écho direct des générateurs et context managers vus précédemment.

Isoler ce qu'on teste vraiment

Un test unitaire ne devrait pas dépendre d'une vraie base de données ou d'un vrai appel réseau, sous peine d'être lent et fragile. Les mocks (unittest.mock) remplacent temporairement une dépendance externe par un objet factice dont on contrôle entièrement le comportement, permettant de tester la logique propre du code sans dépendre de son environnement.

Bonne pratique

Un test unitaire qui appelle une vraie API météo pour tester ServiceMeteo.temperature_actuelle() est lent, fragile (que se passe-t-il si l'API est down ?) et coûteux à grande échelle. Remplacez le client HTTP par un Mock dont vous contrôlez la réponse : le test devient instantané et fiable, peu importe l'état du réseau.

Commandes & code

Tests avec pytest : fixtures et mocks

Écrire des tests fiables, lisibles et rapides.

python
# fichier : calculatrice.py
class Calculatrice:
    def additionner(self, a, b):
        return a + b

    def diviser(self, a, b):
        if b == 0:
            raise ZeroDivisionError("Division par zero")
        return a / b


class ServiceMeteo:
    def __init__(self, client_api):
        self.client_api = client_api

    def temperature_actuelle(self, ville):
        reponse = self.client_api.get(f"/meteo/{ville}")
        return reponse["temperature"]
python
# fichier : test_calculatrice.py
import pytest
from calculatrice import Calculatrice, ServiceMeteo

# Test simple : fonction qui commence par test_
def test_addition():
    calc = Calculatrice()
    assert calc.additionner(2, 3) == 5

def test_division_par_zero_leve_exception():
    calc = Calculatrice()
    with pytest.raises(ZeroDivisionError):
        calc.diviser(10, 0)

# Parametrize : executer le meme test avec plusieurs jeux de donnees
@pytest.mark.parametrize("a, b, attendu", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
    (100, -50, 50),
])
def test_addition_parametree(a, b, attendu):
    calc = Calculatrice()
    assert calc.additionner(a, b) == attendu

# Fixture : prepare des donnees/objets reutilisables entre tests
@pytest.fixture
def calculatrice():
    print("Setup de la fixture")
    calc = Calculatrice()
    yield calc                      # tout ce qui suit yield = teardown
    print("Teardown de la fixture")

def test_avec_fixture(calculatrice):
    assert calculatrice.additionner(1, 1) == 2

# Fixtures avec portee (scope) : "function" (defaut), "module", "session"
@pytest.fixture(scope="module")
def connexion_bdd_partagee():
    print("Connexion BDD ouverte une seule fois pour tout le module")
    connexion = {"active": True}
    yield connexion
    print("Connexion BDD fermee")

# Fixtures parametrees
@pytest.fixture(params=[1, 2, 3])
def nombre(request):
    return request.param

def test_carre_positif(nombre):
    assert nombre ** 2 >= 0

# --- Mocks : isoler le code teste de ses dependances externes ---
from unittest.mock import Mock, MagicMock, patch

def test_service_meteo_avec_mock():
    client_mock = Mock()
    client_mock.get.return_value = {"temperature": 22}

    service = ServiceMeteo(client_mock)
    resultat = service.temperature_actuelle("Paris")

    assert resultat == 22
    client_mock.get.assert_called_once_with("/meteo/Paris")   # verifie l'appel

# patch : remplacer temporairement un objet/fonction pendant le test
def appeler_api_externe():
    import requests
    return requests.get("https://api.example.com/data").json()

@patch("requests.get")
def test_appel_api_mocke(mock_get):
    mock_get.return_value.json.return_value = {"cle": "valeur"}
    resultat = appeler_api_externe()
    assert resultat == {"cle": "valeur"}

# patch en tant que context manager
def test_avec_patch_context():
    with patch("requests.get") as mock_get:
        mock_get.return_value.status_code = 200
        import requests
        reponse = requests.get("https://example.com")
        assert reponse.status_code == 200

# side_effect : simuler des comportements dynamiques (exceptions, sequences de valeurs)
def test_side_effect():
    mock = Mock(side_effect=[1, 2, ValueError("erreur simulee")])
    assert mock() == 1
    assert mock() == 2
    with pytest.raises(ValueError):
        mock()

# Marqueurs : ignorer, isoler ou categoriser des tests
@pytest.mark.skip(reason="Fonctionnalite pas encore implementee")
def test_fonctionnalite_future():
    ...

@pytest.mark.slow
def test_traitement_lourd():
    ...
bash
pytest                              # lance tous les tests du dossier courant
pytest test_calculatrice.py         # un fichier precis
pytest -k "addition"                 # filtre par nom de test
pytest -v                             # mode verbeux
pytest --cov=mon_package              # couverture de code (necessite pytest-cov)
pytest -m "not slow"                   # exclut les tests marques "slow"

Résumé

  • Les fixtures pytest gèrent le setup/teardown ; yield sépare préparation et nettoyage.
  • @pytest.mark.parametrize évite la duplication pour tester plusieurs cas.
  • unittest.mock.Mock/patch isolent le code testé de ses dépendances externes (API, BDD...).
  • assert_called_once_with vérifie qu'une dépendance mockée a été appelée comme attendu.

Exercices pratiques

1 disponible
1

Mission : fiabiliser des tests météo qui dépendent d'internet

Objectif : Isoler un service de test d'une dépendance réseau réelle avec des mocks, et supprimer la duplication de tests avec parametrize.

Contexte

Les tests de ServiceMeteo.temperature_actuelle() appellent réellement une API météo publique à chaque exécution de la CI. Certains jours, les tests échouent parce que l'API est temporairement indisponible, ce qui n'a rien à voir avec la qualité du code testé. En parallèle, test_addition_1, test_addition_2 et test_addition_3 dupliquent presque le même code trois fois.

Tu dois remplacer le client réseau par un mock contrôlé, factoriser les trois tests dupliqués, et comprendre exactement ce que vérifie assert_called_once_with.

Résoudre l’exercice →