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 modulemain
grâce à la lignemonkeypatch.setattr(main, 'request', mockreturn)
.
À quoi correspondent les 3 arguments ?
main
: le module qui contient la fonctionrequest
.request
: une chaîne de caractères contenant le nom de la fonction.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 classePlayer
et 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 classePlayer
tout entière. En effet, nous avons tout d’abord créé une classeMockResponse
qui contient l’ensemble des méthodes de la classe que nous souhaitons mocker. Et la fonctionmock_get
renvoie une instance de la classeMockResponse
qui 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 dePlayer
sera 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 fixturemocker
vous 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.py
pour 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 constantePI
dans le modulecircle
par3,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éthoderequest
avec 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 fonctionrequest
afin 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 modulemain
et 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 module
Controller
avec 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éthode
monkeypatch.setattr()
qui permet de modifier le comportement de fonctions, de méthodes ou même d’objets.La méthode
mocker.patch.object()
permet de mocker une constante.La méthode
mocker.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 !