• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 25/04/2022

Utilisez des mocks pour simuler le comportement d’un objet

Les mocks permettent de simuler essentiellement le comportement d’un objet, mais aussi les constantes ou les fonctions. Les mocks sont donc une composante importante pour la mise en place de test unitaire, car ils permettent de tester notre unité de code en maîtrisant le comportement des dépendances.

Dans le cas d’un langage de programmation dynamique comme Python, nous parlons de monkey patching. Car dans ce cas, lors de l’appel de l'objet ou de la méthode, leur comportement sera modifié dynamiquement afin de renvoyer une ou des valeurs préalablement spécifiées dans le test.

Nous avons vu dans les chapitres précédents qu'un test unitaire permet de tester un bloc de code, et notamment tester la logique de celui-ci. Il est important de noter que dans la logique des tests unitaires, nous testons simplement le comportement interne d’une fonction ou d’une méthode, en fournissant les paramètres d’entrée et en vérifiant les paramètres de sortie. C’est-à-dire que nous n’avons pas besoin de tester le comportement des fonctions ou des méthodes externes à notre unité de code. En effet, cette fonction externe aura son propre test unitaire.

C’est pourquoi nous allons simuler le comportement de ces objets ou fonctions afin d’implémenter nos tests tranquillement, sans nous soucier des dépendances externes.

Mais pourquoi ne pas tout simplement tester en même temps l’ensemble des parties du code sans utiliser de mock ? C’est plus simple !

Vous risquez tout simplement de casser la totalité des tests de votre application en effectuant une petite modification dans une classe. Il faut ainsi toujours isoler l’unité de code que vous souhaitez tester. Et pour cela nous avons les mocks.

Les mocks trouvent leur principale utilité dans les tests unitaires, mais trouvent un intérêt dans plusieurs situations :

  • Imiter la réponse d’une requête API : Il nous faut pouvoir lancer nos tests sans avoir besoin d'Internet. Cela vous paraît étrange ? Mais imaginez que vous travailliez dans le train ! De plus, lancer des tests doit être très rapide. Or, chaque appel HTTP sera plus lent qu'un simple retour de l'ordinateur.

  • Imiter l'écriture dans un fichier : Afin de ne pas créer ni supprimer des fichiers à chaque lancement de test dans notre dossier de travail.

  • Travailler en équipe sur un projet : Si vous travaillez en équipe et que vous êtes dépendant du travail d’un collègue, grâce aux mocks vous allez pouvoir simuler le comportement du code de votre collègue. Maintenant, vous n’avez plus besoin d’attendre que votre collègue termine son code pour commencer votre partie.

  • Reproduire des cas d’erreur : Il est parfois impossible de reproduire un scénario de test qui reproduit un cas d’erreur, comme un problème de réseau, une base de données absente ou même une API qui ne fonctionne plus temporairement.

  • Test Driven Development : Vous ne connaissez peut-être pas encore le principe du Test Driven Development (TDD). Pour vous donner une idée globale, pratiquer le TDD, c’est implémenter d’abord les tests avant le code source. Nous verrons ce principe de développement dans un chapitre plus loin.

Il existe bien sûr d'autres raisons d'utiliser les mocks. Pour le reste, vous allez pouvoir le découvrir par vous-même. En attendant, je vais vous montrer comment créer et utiliser un mock à l’aide de monkeypatch et pytest-mock.

Découvrez monkeypatch, une solution pour mocker

Monkeypatch fournit un ensemble de méthodes pour mocker nos fonctionnalités : 

  • monkeypatch.setattr(obj, name, value, raising=True): Modifier le comportement d’une fonction ou d’une classe.

  • monkeypatch.delattr(obj, name, raising=True): Supprimer la fonction du test.

  • monkeypatch.setitem(mapping, name, value): Modifier les éléments d’un dictionnaire.

  • monkeypatch.delitem(obj, name, raising=True): Supprimer un élément d’un dictionnaire.

  • monkeypatch.setenv(name, value, prepend=False): Définir une variable d’environnement.

  • monkeypatch.delenv(name, raising=True): Supprimer une variable d’environnement.

Voyons maintenant quelques exemples d’utilisation pour certaines de ces méthodes.

Monkeypatch une fonction

Si vous souhaitez simuler le comportement d’une fonction lors de vos tests, vous allez devoir utiliser la fonctionmonkeypatch.setattr().

Pour l’exemple, nous allons prendre une fonction qui appelle une autre fonction indépendante qui exécute des requêtes sur des API. Il est donc inutile de tester la fonction externe qui va faire des requêtes sur des API, car cette fonction aura son propre test unitaire. De plus, la fonction risque de prendre beaucoup de temps à l’exécution. Nous allons donc simuler le comportement de cette fonction à l’aide d’un mock.

Voici la fonctionrequest()qui fait des appels sur des API et qui retourne 10. Dans notre cas, nous allons mettre une pause de 10 secondes pour imiter le temps d’exécution des requêtes :

import time
 
def request():
    time.sleep(10)
    return 10

Ensuite, nous allons implémenter la fonction qui appelle cette fameuse fonction, que nous nommeronsmain_function():

def main_function():
    response = request()
    return response

Nous pouvons maintenant vérifier avec un test unitaire que notre fonction renvoie bien la réponse reçue de la fonctionrequest(), mais nous allons mocker cette fonction et changer la valeur de retour : 100 à la place de 10.

D’ailleurs, vous allez aussi constater que le test durera une fraction de seconde et pas les 10 secondes que nous avons prévues dans la fonctionrequest(). Effectivement, quand nous mockons une fonction, nous ne lançons pas la fonction mais simulons la valeur de retour de cette fonction. 

Voici le test unitaire (nous allons considérer que les deux fonctions précédentes sont contenues dans un même fichiermain.py) :

import main
from main import main_function
 
def test_main_function(monkeypatch):
 
    def mockreturn():
        return 100
 
    monkeypatch.setattr(main, 'request', mockreturn)
 
    expected_value = 100
    assert main_function() == expected_value

Dans l’exemple ci-dessus, nous avons modifié le comportement de la fonctionrequest() contenue dans le modulemaingrâce à la lignemonkeypatch.setattr(main, 'request', mockreturn).

À quoi correspondent les 3 arguments ?

  1. main: le module qui contient la fonctionrequest.

  2. request: une chaîne de caractères contenant le nom de la fonction.

  3. mockreturn: la fonction qui renvoie la valeur à remplacer.

Monkeypatch un objet

Nous allons cette fois mocker le comportement d’une classe et simuler les méthodes de celle-ci.

Considérons la classe suivante :

class Player:
    def __init__(self, name, level):
        self.name = name
        self.level = level
 
    def get_info(self):
        infos = {"name" : self.name,
        "level" : self.level}
        return infos

Et voici la fonctioncreate_player()qui instanciera la classePlayeret appellera la méthode  get_info():

def create_player():
    player = Player("Ranga", 100)
    infos = player.get_info()
    return infos

Et maintenant, nous pouvons implémenter le test suivant qui simulera la valeur de retour de la fonctionget_info():

import main
from main import create_player
 
class MockResponse:
 
    @staticmethod
    def get_info():
        return {"name": "test", "level" : 200}
 
def test_create_player(monkeypatch):
 
    def mock_get(*args, **kwargs):
        return MockResponse()
 
    monkeypatch.setattr('main.Player', mock_get)
 
    expected_value = {"name": "test", "level" : 200}
    assert create_player() == expected_value

En réalité, nous n’avons pas mocké simplement la méthodeget_info(), mais la classePlayertout entière. En effet, nous avons tout d’abord créé une classeMockResponsequi contient l’ensemble des méthodes de la classe que nous souhaitons mocker. Et la fonctionmock_getrenvoie une instance de la classeMockResponsequi définit le nouveau comportement de la méthodeget_info().

Par la suite, lorsque la lignemonkeypatch.setattr('main.Player', mock_get)sera exécutée, l’instance dePlayersera remplacée par l’instance deMockResponse.  C’est pour cette raison que la fonctionget_info()retourne le dictionnaire{"name": "test", "level" : 200}.

Utilisez pytest-mock pour mocker

Pytest nous fournit un super plugin pour gérer le mocking dans nos projets plus facilement. Pour l’utiliser, vous devez tout d’abord installer le plugin pytest-mock à l’aide de la commande suivante : 

pip install pytest-mock

Avec pytest-mock, grâce à la fixturemockervous pouvez mocker :

  • une constante ;

  • une fonction ou une méthode ;

  • un objet.

Voyons ensemble comment faire !

Mockez une constante

Parfois, vous ne pouvez pas définir une constante sans lancer la vraie application. Certaines constantes sont assignées lors du lancement de l’application en lisant des variables d’environnement, ou tout simplement en lisant un fichier de configuration. Il est donc impossible de connaître la valeur de la constante lors des tests unitaires.

Prenons la fonction suivante que nous avons implémentée dans un fichiercircle.py. La fonction renvoie le périmètre d’un cercle en utilisant la constantePI:

PI = 3,1415
 
def perimeter(radius):
    return 2 * PI * radius

Si vous souhaitez tester votre fonction avec une autre valeur de la constantePI, nous pouvons utiliser la fonctionmocker.patch.object()et remplacer la constante par une autre valeur.

Nous allons donc créer un fichier de testtest_circle.pypour tester la fonctionperimeter()avecPI = 3,14.

import circle
from circle import perimeter
 
def test_should_return_perimeter(mocker):
    mocker.patch.object(circle, 'PI', 3.14)
    expected_value = 12.56
    assert perimeter(2) == expected_value

Cette lignemocker.patch.object(circle, 'PI', 3.14)permet de modifier la valeur de la constantePIdans le modulecirclepar3,14. Ainsi, cette méthode nous permet de tester notre fonction avec une valeur choisie au préalable lors de l’implémentation du test.

Mockez une fonction

Reprenons l’exemple que nous avions pris pour mocker la méthoderequestavec monkeypatch.

Voici le code source :

import time
 
def request():
    time.sleep(10)
    return 10
 
def main_function():
    response = request()
    return response

Comme précédemment, nous allons mocker la fonctionrequestafin qu'elle renvoie la valeur 100 à la place de 10.

Cette fois, nous avons le test unitaire suivant :

import main
from main import main_function
 
def test_main_function(mocker):
    mocker.patch('main.request', return_value=100)
 
    expected_value = 100
    assert main_function() == expected_value

 Dans l’exemple ci-dessus, nous avons modifié le comportement de la fonctionrequest contenue dans le modulemainet nous avons remplacé la valeur de retour par la valeur 100 (return_value = 100),  grâce à la lignemocker.patch('main.request'  , return_value=100)  .

Dans le screencast ci-dessous, vous allez voir comment mocker une méthode de classe comme si ce n’était qu’une simple fonction. Vous allez voir qu’il est aussi possible de mocker simplement une seule méthode d’une classe.

Mockez un objet

Concernant les classes, nous reprendrons une nouvelle fois le code source de la partie monkeypatch et verrons la différence avec pytest-mock.

Voici le code source :

class Player:
    def __init__(self, name, level):
        self.name = name
        self.level = level
 
    def get_info(self):
        infos = {"name" : self.name,
        "level" : self.level}
        return infos
 
def create_player():
    player = Player("Ranga", 100)
    infos = player.get_info()
    return infos

Avec pytest-mock, vous avez besoin d’implémenter la classeMockReponse, qui définit les méthodes à simuler, et vous configurez le mock à l’aide de la lignemocker.patch('main.Player', return_value = MockResponse()). Le premier argument contient le nom de la classe que vous souhaitez simuler, et le deuxième argument l’instance par laquelle vous souhaitez la remplacer.

Nous aurons le test suivant :

import main
from main import create_player
 
class MockResponse:
 
    @staticmethod
    def get_info():
        return {"name": "test", "level" : 200}
 
def test_create_player(mocker):
 
    mocker.patch('main.Player', return_value = MockResponse())
 
    expected_value = {"name": "test", "level" : 200}
    assert create_player() == expected_value

N’hésitez pas à reprendre l’ensemble de ces exemples et à les lancer dans votre environnement.

À vous de jouer !

Reprenons le projet de la super-calculatrice pour ajouter les tests unitaires du moduleController  .

Votre mission :

  • Créez la suite de tests concernant le moduleControlleravec pytest-mock.

Retrouvez une proposition de correction sur GitHub !

En résumé

  • Les mocks permettent de simuler le comportement d’une méthode ou d’un objet.

  • Monkeypatch fournit la méthodemonkeypatch.setattr()qui permet de modifier le comportement de fonctions, de méthodes ou même d’objets.

  • La méthodemocker.patch.object()permet de mocker une constante.

  • La méthodemocker.patch()permet de mocker une fonction ou un objet.

Grâce aux mocks, vous savez maintenant totalement isoler une unité de code afin d’implémenter vos tests unitaires. Nous pouvons maintenant aller plus loin et voir comment construire des tests unitaires sur une application web utilisant le framework Flask. Ne perdons pas de temps !

Exemple de certificat de réussite
Exemple de certificat de réussite