Contrôler les dépendances externes
Objectif : Tester l’enregistrement d’une dépense sans appeler réellement le stockage externe.
Un service peut calculer correctement une dépense tout en échouant lorsqu’il la transmet au stockage. Le test ne doit pas dépendre d’une base de données, d’un fichier ou d’un service distant : vous allez remplacer cette dépendance par un objet simulé et vérifier l’interaction attendue.
Vous avez déjà appris à isoler une règle métier avec pytest et à couvrir plusieurs entrées par paramétrage. Ici, le résultat à contrôler n’est pas seulement une valeur retournée : c’est aussi le fait que le service demande au stockage d’enregistrer la dépense attendue, une seule fois.
Définir la dépendance du service
Le service reçoit une source de dépenses au lieu de créer lui-même un accès au stockage. Ce contrat permet de fournir au service une implémentation en mémoire dans le fonctionnement courant, puis un mock dans le test. L’exemple ci-dessous fixe les signatures utilisées dans cette leçon : si votre projet possède déjà ces éléments, conservez ses noms et ses champs plutôt que de les dupliquer.
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Depense:
description: str
montant: Decimalfrom typing import Protocol
from depenses.modeles import Depense
class SourceDepenses(Protocol):
def ajouter(self, depense: Depense) -> None:
"""Enregistre une dépense."""
class SourceDepensesMemoire:
def __init__(self) -> None:
self.depenses: list[Depense] = []
def ajouter(self, depense: Depense) -> None:
self.depenses.append(depense)
def selectionner(source: SourceDepenses) -> list[Depense]:
return list(source.depenses) # À remplacer par la lecture prévue par votre contrat.
def enregistrer(source: SourceDepenses, depense: Depense) -> None:
source.ajouter(depense)Préparer un test avec une fixture et un mock
Une fixture pytest prépare une valeur réutilisable par les tests. Ici, elle construit une dépense de référence. Le test crée ensuite un mock conforme à l’interface SourceDepenses : l’option spec empêche d’utiliser par erreur une méthode qui n’existe pas sur le contrat fourni.
from decimal import Decimal
from unittest.mock import create_autospec
import pytest
from depenses.modeles import Depense
from depenses.service import SourceDepenses, enregistrer
@pytest.fixture
def depense() -> Depense:
return Depense(description="Repas", montant=Decimal("12.50"))
def test_enregistrer_transmet_la_depense_au_stockage(depense: Depense) -> None:
source = create_autospec(SourceDepenses, instance=True)
enregistrer(source, depense)
source.ajouter.assert_called_once_with(depense)L’appel à enregistrer constitue l’action du test. L’assertion assert_called_once_with vérifie deux propriétés à la fois : ajouter a été appelé exactement une fois et l’argument transmis est bien l’objet Depense attendu. Si le service oublie l’écriture, la répète ou transmet une autre dépense, pytest fait échouer ce test.
python -m pytest -q tests/test_service.py1 passedVérifier les appels adaptés au contrat
Le mock n’écrit rien sur disque et n’ouvre aucune connexion. Il enregistre les interactions reçues afin que vous puissiez les examiner après l’appel. Utilisez une assertion précise, adaptée au contrat métier : vérifier seulement que la méthode a été appelée ne détecterait pas un argument erroné ou un appel répété.
La fixture vous évite de reconstruire la même donnée dans plusieurs tests et nomme clairement ce qui est fourni. Vous pouvez également créer une fixture pour le mock si plusieurs tests de service partagent cette préparation. Ne confondez pas toutefois une fixture, qui prépare et fournit une ressource, avec le mock, qui simule le comportement d’une dépendance.
À retenir
- Injectez la source de stockage dans le service au lieu de la créer pendant l’opération métier.
- Utilisez une fixture pytest pour fournir des données de test explicites et réutilisables.
- Simulez la dépendance externe avec un mock conforme à son interface.
- Vérifiez la méthode appelée, ses arguments et le nombre d’appels attendu.
- Lancez les tests avec python -m pytest -q tests/test_service.py.