cours
Tutoriel pytest-mock : Un guide pour débutants sur le Mocking en Python
La complexité et la taille de vos projets Python augmentent, tout comme l'importance de tests robustes. Les tests permettent de s'assurer que votre code fonctionne comme prévu et de maintenir la qualité dans toutes les parties de l'application.
Lorsque votre application interagit avec différents types de bases de données ou d'API externes comme OpenAI GPT ou Anthropic Claude, tester ces dépendances peut s'avérer difficile. C'est là que la simulation s'avère utile.
Le mocking vous permet de simuler des objets, des fonctions et des comportements, ce qui rend vos tests plus isolés et plus prévisibles. Elle vous permet également de vous concentrer sur la fonction spécifique que vous souhaitez tester en l'isolant des dépendances susceptibles d'introduire de la complexité ou un comportement inattendu.
Dans ce blog, vous apprendrez à utiliser pytest-mock
, un puissant plugin pour pytest, pour implémenter efficacement des mocks dans vos tests Python. À lafin de ce tutoriel, vous serez prêt à ajouter des techniques de mocking à vos cas de test, les rendant ainsi plus puissants.
Qu'est-ce que pytest-mock ?
pytest-mock est un plugin pour le célèbre framework de test Python pytest qui fournit un accès faciley aux capacités de mocking. Il s'appuie sur le système intégré de Python unittest
, ce qui simplifie le processus de simulation lors des tests.
logo du framework pytest
pytest-mock
améliore la lisibilité et facilite l'implémentation des mocks dans les tests avec une approche plus pytest-native. Que vous ayez besoin de simuler des fonctions individuelles, des méthodes de classe ou des objets entiers, pytest-mock
offre la flexibilité nécessaire pour des tests efficaces sans complexité supplémentaire.
L'une des principales raisons de la lisibilité de pytest-mock
est son style de test déclaratif, qui vous permet de spécifier les appels et les comportements attendus directement dans vos tests. Le code est ainsi plus facile à écrire, à lire et à maintenir, tout en restant résistant aux changements d'implémentation qui ne modifient pas le comportement externe du code.
Devenez développeur Python
Mise en place de pytest-mock
La création d'un environnement virtuel pour votre projet est une bonne pratique pour s'assurer que les dépendances sont isolées. Si vous n'avez pas encore mis en place un nouvel environnement, vous pouvez créer un environnement conda en utilisant les commandes suivantes :
# create a conda environment
conda create --name yourenvname python=3.11
# activate conda environment
conda activate yourenvname
Installation de pytest et pytest-mock
Installez pytest
et pytest-mock
directement dans votre environnement en utilisant pip ou conda :
pip install pytest
pip install pytest-mock
Confirmation de l'installation
Après avoir installé pytest
et pytest-mock
, vérifiez que l'installation a réussi en exécutant la commande suivante :
pytest --version
Les bases de la simulation avec pytest-mock
L'imitation peut être considérée comme la création d'une version "factice" d'un composant qui imite le comportement réel.
Elle est particulièrement utile lorsque vous devez tester la façon dont les fonctions ou les classes interagissent avec des composants externes tels que des bases de données ou des API externes.
Introduction à l'imitation
Un objet fictif simule le comportement d'un objet réel. Il est souvent utilisé dans les tests unitaires pour isoler les composants et tester leur comportement sans exécuter le code dépendant.
Par exemple, vous pouvez utiliser un objet fictif pour simuler un appel à la base de données sans vous connecter à la base de données.
La simulation est particulièrement utile lorsque l'objet réel :
- est difficile à créer ou à configurer.
- est long à utiliser (par exemple, l'accès à une base de données distante).
- A des effets secondaires qu'il convient d'éviter pendant les tests (par exemple, envoi de courriers électroniques, coûts).
Utilisation de l'outil de simulation
pytest-mock fournit une fixation mocker
, facilitant la création et le contrôle d'objets fictifs. La fixation mocker
est powerful et s'intègre dans vos tests pytest.
💡Qu'est-ce qu'un luminaire ?En Python, les fixtures sont des composants réutilisables qui mettent en place et démontent les ressources nécessaires aux tests, comme les bases de données ou les fichiers. Ils garantissent la cohérence, réduisent les doublons et simplifient la mise en place des tests. |
Prenons un exemple :
def fetch_weather_data(api_client):
response = api_client.get("https://api.weather.com/data")
if response.status_code == 200:
return response.json()
else:
return None
La fonction fetch_weather_data
dépend d'une API météorologique externe pour récupérer les données, mais l'appel de l'API pendant les tests pourrait entraîner des coûts inutiles.
Pour éviter cela, vous pouvez utiliser le mocking pour simuler le comportement de l'API dans vos tests, en veillant à ce que l'application soit testée sans effectuer d'appels externes. Voici comment la simulation peut vous aider :
def test_fetch_weather_data(mocker):
# Create a mock API client
mock_api_client = mocker.Mock()
# Mock the response of the API client
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temperature": "20C", "condition": "Sunny"}
# Set the mock API client to return the mocked response
mock_api_client.get.return_value = mock_response
# Call the function with the mock API client
result = fetch_weather_data(mock_api_client)
# Assert that the correct data is returned
assert result == {"temperature": "20C", "condition": "Sunny"}
mock_api_client.get.assert_called_once_with("https://api.weather.com/data")
Dans test_fetch_weather_data()
, nous utilisons la fixation mocker
pour créer un client API fictif mock_api_client
et un mock_response
. La réponse fictive est configurée pour imiter un appel API réel en renvoyant un code d'état 200 et des données JSON contenant des informations météorologiques.
La méthode get du client API fictif est définie pour renvoyer cette réponse fictive. Lorsque fetch_weather_data()
est testé, il interagit avec le client API fictif au lieu d'effectuer un appel API réel.
Cette approche permet de s'assurer que la fonction se comporte comme prévu tout en maintenant les tests rapides, économiques et indépendants des systèmes externes.
L'instruction assert à la fin vérifie que les données renvoyées correspondent à la sortie attendue.
La dernière ligne, mock_api_client.get.assert_called_once_with("https://api.weather.com/data")
, vérifie spécifiquement que la méthode get du client API fictif a été appelée exactement une fois et avec l'argument correct. Cela permet de confirmer que la fonction interagit avec l'API de la manière attendue, ajoutant ainsi une couche supplémentaire de validation au test.
Cas d'utilisation courants pour pytest-mock
Le mocking est utile dans différents scénarios, notamment dans les tests unitaires où vous devez isoler des fonctions ou des composants spécifiques. Examinons quelques cas d'utilisation courants :
1. Fonctions ou méthodes de classe de moquerie
Il se peut que vous deviez tester une fonction qui dépend d'autres fonctions ou méthodes, mais que vous souhaitiez contrôler leur comportement pendant le test. Mocking vous permet de remplacer ces dépendances par un comportement prédéfini :
# production code
def calculate_discount(price, discount_provider):
discount = discount_provider.get_discount()
return price - (price * discount / 100)
# test code
def test_calculate_discount(mocker):
# Mock the get_discount method
mock_discount_provider = mocker.Mock()
mock_discount_provider.get_discount.return_value = 10 # Mocked discount value
# Call the function with the mocked dependency
result = calculate_discount(100, mock_discount_provider)
# Assert the calculated discount is correct
assert result == 90
mock_discount_provider.get_discount.assert_called_once()
Dans ce test, la méthode get_discount()
du site discount_provider
est simulée pour renvoyer une valeur prédéfinie (10 %). Cela permet d'isoler la fonction calculate_discount()
de la mise en œuvre effective de discount_provider
.
2. Mocking du code dépendant du temps
Lorsque vous testez des fonctions qui impliquent du temps ou des délais, l'utilisation de méthodes liées au temps permet d'éviter d'attendre ou de manipuler le temps réel :
# production code
import time
def long_running_task():
time.sleep(5) # Simulate a long delay
return "Task Complete"
# test code
def test_long_running_task(mocker):
# Mock the sleep function in the time module
mocker.patch("time.sleep", return_value=None)
# Call the function
result = long_running_task()
# Assert the result is correct
assert result == "Task Complete"
Ici, time.sleep
est patché pour contourner le délai réel pendant les tests. Cela garantit que le test s'exécute rapidement, sans attendre 5 secondes.
3. Attributs de l'objet d'imitation
Il peut arriver que vous souhaitiez simuler le comportement des attributs ou des propriétés d'un objet afin de simuler différents états lors des tests :
# production code
class User:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def is_adult(self):
return self.age >= 18
# test code
def test_user_is_adult(mocker):
# Create a User object
user = User(name="Alice", age=17)
# Mock the is_adult property
mocker.patch.object(User, "is_adult", new_callable=mocker.PropertyMock, return_value=True)
# Assert the mocked property value
assert user.is_adult is True
Dans cet exemple, la propriété is_adult()
de la classe User
est simulée pour renvoyer True, quel que soit l'âge réel. Ceci est utile pour tester des scénarios qui dépendent de l'état des objets.
Techniques avancées de mise en attente avec pytest-mock
Une fois que vous êtes à l'aise avec le mocking de base, pytest-mock
offre des fonctionnalités avancées pour gérer des scénarios plus complexes.
1. Effets secondaires fictifs
Les objets fantaisie peuvent simuler plus que de simples valeurs de retour - ils peuvent également reproduire des comportements tels que la levée d'exceptions ou la modification dynamique des valeurs de retour. Pour ce faire, on ajoute des "effets secondaires" aux objets fantaisie :
import pytest
# production code
def process_payment(payment_gateway, amount):
response = payment_gateway.charge(amount)
if response == "Success":
return "Payment processed successfully"
else:
raise ValueError("Payment failed")
# test code
def test_process_payment_with_side_effects(mocker):
# Mock the charge method of the payment gateway
mock_payment_gateway = mocker.Mock()
# Add side effects: Success on first call, raise exception on second call
mock_payment_gateway.charge.side_effect = ["Success", ValueError("Insufficient funds")]
# Test successful payment
assert process_payment(mock_payment_gateway, 100) == "Payment processed successfully"
# Test payment failure
with pytest.raises(ValueError, match="Insufficient funds"):
process_payment(mock_payment_gateway, 200)
# Verify the mock's behavior
assert mock_payment_gateway.charge.call_count == 2
La propriété side_effect
permet au simulacre de renvoyer des valeurs différentes ou de soulever des exceptions lors d'appels ultérieurs. Dans cet exemple, le premier appel à charge renvoie "Success", tandis que le second appelle ValueError
. Cette fonction est utile pour tester plusieurs scénarios en un seul essai.
2. Espionnage des fonctions
Le cursus vous permet de suivre la façon dont une fonction réelle a été appelée, y compris le nombre d'appels et les arguments passés. Elle est particulièrement utile lorsque vous voulez vous assurer qu'une fonction est appelée comme prévu tout en continuant à exécuter sa logique originale :
# production code
def log_message(logger, message):
logger.info(message)
return f"Logged: {message}"
# test code
def test_log_message_with_spy(mocker):
# Spy on the info method of the logger
mock_logger = mocker.Mock()
spy_info = mocker.spy(mock_logger, "info")
# Call the function
result = log_message(mock_logger, "Test message")
# Assert the returned value
assert result == "Logged: Test message"
# Verify the spy behavior
spy_info.assert_called_once_with("Test message")
assert spy_info.call_count == 1
La méthode spy()
garde la trace de la manière dont une méthode réelle est appelée sans en modifier le comportement. Ici, nous espionnons la méthode info du logger pour nous assurer qu'elle est appelée avec le bon message tout en laissant la méthode s'exécuter normalement.
Meilleures pratiques pour l'utilisation de pytest-mock
Mocking est un outil puissant, mais l'utiliser sans précaution peut entraîner des problèmes dans vos tests. Voici quelques bonnes pratiques que je vous recommande de suivre :
1. Évitez la surenchère
L'utilisation d'un trop grand nombre de mocks peut conduire à des tests fragiles qui s'interrompent lorsque le code interne est modifié, même si le comportement externe ne l'est pas. Efforcez-vous de ne simuler que ce qui est nécessaire et de vous appuyer sur des applications réelles chaque fois que cela est possible.
Le sur-mockage peut rendre vos tests étroitement liés aux détails d'implémentation de votre code, ce qui signifie que même une refonte mineure peut faire échouer les tests inutilement. Au lieu de cela, concentrez-vous sur la simulation des parties du système qui sont externes ou qui ont des effets secondaires importants.
2. Utiliser des conventions de dénomination claires
Lorsque vous mockez des objets, utilisez des noms descriptifs pour vos mocks. Cela rend vos tests plus lisibles et plus faciles à maintenir. Par exemple, au lieu d'utiliser des noms génériques comme mock_function()
, utilisez quelque chose de plus descriptif, comme mock_api_response()
.
Des conventions de dénomination claires aident les autres à comprendre l'objectif de chaque simulacre, réduisant ainsi la confusion et facilitant la maintenance de la suite de tests au fil du temps.
3. Gardez les tests simples et ciblés
Chaque test doit porter sur un seul comportement ou scénario. Cela simplifie le débogage et rend vos tests faciles à comprendre. Un test bien ciblé est plus susceptible de fournir un retour d'information clair lorsque quelque chose ne va pas, ce qui facilite l'identification et la résolution des problèmes.
Conclusion
Dans ce blog, nous avons exploré les bases du mocking avec pytest-mock
et appris à l'utiliser pour améliorer nos tests Python. Nous avons tout abordé, de la simulation de fonctions et de méthodes à des techniques plus avancées telles que l'ajout d'effets secondaires et l'espionnage de fonctions.
Mocking est un outil essentiel pour créer des tests fiables et faciles à maintenir, en particulier lorsque vous travaillez avec des systèmes complexes ou des dépendances externes. En intégrant pytest-mock
dans vos projets, vous pouvez écrire des tests plus isolés, plus prévisibles et plus faciles à maintenir.
Pour continuer à développer vos compétences en matière de tests, je vous recommande de consulter lecours gratuit Introduction aux tests en Python sur DataCamp.
Renforcer les compétences en matière d'apprentissage automatique
FAQ
En quoi la simulation diffère-t-elle du test de composants réels ?
La simulation remplace les composants réels par des composants simulés, ce qui permet d'effectuer des tests isolés et rentables sans dépendre de systèmes externes ou de données réelles.
Puis-je utiliser pytest-mock avec d'autres frameworks de test ?
pytest-mock
est spécialement conçu pour pytest
et s'y intègre parfaitement, mais il s'appuie sur le site unittest.mock
de Python, qui peut être utilisé indépendamment.
Les fixtures sont-elles obligatoires pour le mocking dans pytest-mock ?
Bien que les fixtures comme mocker améliorent l'efficacité du mocking, vous pouvez toujours utiliser unittest.mock
directement dans pytest
sans fixtures, bien qu'avec une intégration moindre.
Apprenez-en plus sur Python avec ces cours !
cours
Introduction à la science des données en Python
cours