Accéder au contenu principal

Tutoriel pytest-mock : Un guide pour débutants sur le Mocking en Python

Apprenez à utiliser pytest-mock, de la configuration et des principes fondamentaux aux techniques avancées pour des tests pratiques.
Actualisé 19 déc. 2024  · 15 min de lecture

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

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

Acquérir les compétences de programmation dont tous les développeurs Python ont besoin.
Commencez À Apprendre Gratuitement

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

Améliorez vos compétences en matière d'apprentissage automatique au niveau de la production.

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.


Moez Ali's photo
Author
Moez Ali
LinkedIn
Twitter

Scientifique de données, fondateur et créateur de PyCaret

Sujets

Apprenez-en plus sur Python avec ces cours !

Certification disponible

cours

Introduction à Python pour les développeurs

3 hr
35.9K
Maîtrisez les fondamentaux de la programmation en Python. Aucune connaissance préalable n'est requise !
Afficher les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow