You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content/articles/mock_unittest.md
+36-36Lines changed: 36 additions & 36 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,21 +11,21 @@ Malgré les blagues et les débats que l'on peut lire concernant les tests unita
11
11
Les test unitaires servent donc à tester un bout de son code de façon isolée et vérifier que ce dernier fasse bien le travail qui lui a été demandé.
12
12
Prenons l'exemple suivant, je dois créer un algorithme qui reçoit en entrée une liste de tuples composée d'un prénom, d'un âge et d'une taille et je dois le retourner sous forme d'un dictionnaire.
13
13
14
-
Pour tester ce code, je vais donc dans la réalisation de mon test simuler une fausse entrée (une liste de tuple) et indiquer le résultat attendu (le dictionnaire que je souhaiterais renvoyer). Je vais appliquer mon algorithme avec cette fausse entrée et comparer la sortie avec le résultat attendu. Si c'est pareil, le test est validé et si ça ne l'est pas, faut retourner travailler.
15
-
C'est logique, c'est propre, c'est net. Malheureusement que faire si l'entrée en question provient d'une source externe comme une API par exemple? Dans ce cas-là, on va avoir un peu plus de difficulté à simuler l'entrée. Autre point, comment fait-on si la sortie de l'algorithme consiste à envoyer un mail à l'utilisateur? Pas évident non plus de comparer les résultats attendus.
14
+
Pour tester ce code, je vais donc dans la réalisation de mon test simuler une fausse entrée (une liste de tuple) et indiquer le résultat attendu (le dictionnaire que je souhaiterais renvoyer). Je vais appliquer mon algorithme avec cette fausse entrée et comparer la sortie avec le résultat attendu. Si c'est pareil, le test est validé et si c'est pas pareil, faut retourner travailler.
15
+
C'est logique, c'est propre, c'est net. Malheureusement que faire si l'entrée en question provient d'une source externe comme une API par exemple? Dans ce cas-là, on va avoir un peu plus de difficulté à simuler l'entrée. Autre point, comment fait-on si la sortie de l'algorithme consiste à envoyer un mail à l'utilisateur? Pas évident non plus de comparer les résultats attendus.
16
16
17
-
Mais ne vous inquiétez pas pour autant, car vous n'êtes pas le premier à être confronté à ce type de problématiques et des solutions ont été trouvées pour y répondre. Et c'est justement l'objectif de cet article qui va vous présenter la magie des Mocks!
17
+
Mais ne vous inquiétez pas pour autant, car vous n'êtes pas le premier à être confronté à ce type de problématiques et des solutions ont été trouvées pour y répondre. Et c'est justement l'objectif de cet article qui va vous présenter la magie des Mocks!
18
18
19
-
## Qu'est-ce qu'un Mock?
19
+
## Qu'est-ce qu'un Mock?
20
20
21
21
Un Mock, comme son nom l'indique (et oui c'est de l'anglais) est un objet qui consiste à imiter un autre objet.
22
-
Conceptuel n'est-ce pas? Tout simplement, un mock c'est ce qui va vous permettre de simuler un retour API sans vraiment appeler une API ou simuler un envoi d'email sans vraiment envoyer un email. Cela permet d'imiter pas mal de choses afin de vous permettre de réaliser vos tests unitaires de façon indépendante.
22
+
Conceptuel n'est-ce pas? Tout simplement, un mock c'est ce qui va vous permettre de simuler un retour API sans vraiment appeler une API ou simuler un envoi d'email sans vraiment envoyer un email. Cela permet d'imiter pas mal de choses afin de vous permettre de réaliser vos tests unitaires de façon indépendante.
23
23
24
-
## Dans la pratique, ça donne quoi?
24
+
## Dans la pratique, ça donne quoi?
25
25
26
26
Rien de mieux qu'un exemple pour appréhender un peu mieux ce concept.
27
-
Imaginons le besoin suivant: je développe un site qui a pour objectif d'identifier des produits de substitution meilleur pour la santé par rapport à un produit donné. Pour cela, je souhaite interroger via l'[API OpenFoodFacts](https://fr.openfoodfacts.org/data) les produits liés à une marque et compter le nombre de produits ayant une bonne note alimentaire.
28
-
Voici le code de ma classe (fichier app.py):
27
+
Imaginons le besoin suivant: je développe un site qui a pour objectif d'identifier des produits de substitution meilleur pour la santé par rapport à un produit donné. Pour cela, je souhaite interroger via l'[API OpenFoodFacts](https://fr.openfoodfacts.org/data) les produits liés à une marque et compter le nombre de produits ayant une bonne note alimentaire.
28
+
Voici le code de ma classe (fichier app.py):
29
29
30
30
```python
31
31
import urllib.error
@@ -93,18 +93,18 @@ class OpenFoodFactsAPI:
93
93
### Objectif de mon test
94
94
95
95
Je souhaite tester ma méthode **count_product_numb()** afin de m'assurer que cette dernière me renvoie bien le bon nombre de produits ayant une bonne note alimentaire.
96
-
Dans notre cas, l'algorithme est censé travailler sur la donnée provenant de l'API récupérée via la méthode **_get_product_from_api()**. Hors, j'y vois deux problèmes pour mon test:
97
-
* D'un point de vue performance, si j'ai 150 tests qui font tous des appels externes, je suis pas prêt de visualiser les résultats de ces derniers.
98
-
* Je n'ai aucune idée du nombre de produits (dont des produits avec une bonne note alimentaire) que l'API va me renvoyer. Je pourrais bien évidemment faire le test à coté et les compter mais qui me dit que dans le temps, des produits ne seront pas rajouté ou supprimé?
96
+
Dans notre cas, l'algorithme est censé travailler sur la donnée provenant de l'API récupérée via la méthode **_get_product_from_api()**. Hors, j'y vois deux problèmes pour mon test:
97
+
* D'un point de vue performance, si j'ai 150 tests qui font tous des appels externes, je ne suis pas prêt de visualiser les résultats de ces derniers.
98
+
* Je n'ai aucune idée du nombre de produits (dont des produits avec une bonne note alimentaire) que l'API va me renvoyer. Je pourrais bien évidemment faire le test à coté et les compter mais qui me dit que dans le temps, des produits ne seront pas rajouté ou supprimé?
99
99
100
100
Du coup, pour tester ma méthode, il faudrait que je puisse simuler ce retour d'API pour avoir une donnée similaire à celle-ci.
101
-
Et bien, c'est tous l'intérêt des mocks justement.
101
+
Et bien, c'est tout l'intérêt des mocks justement.
102
102
103
103
### Mise en place de mon test
104
104
105
-
Pour mettre en place ce test, je vais utiliser le module unittest. L'avantage de ce module, c'est qu'il est directement intégré dans Python et qu'il dispose de la possibilité de mettre en place des mocks, et même de plusieurs façons différentes!
105
+
Pour mettre en place ce test, je vais utiliser le module unittest. L'avantage de ce module, c'est qu'il est directement intégré dans Python et qu'il dispose de la possibilité de mettre en place des mocks, et même de plusieurs façons différentes!
106
106
107
-
Je mets donc en place la structure de test suivante (fichier test_app.py):
107
+
Je mets donc en place la structure de test suivante (fichier test_app.py):
108
108
109
109
```python
110
110
from app import OpenFoodFactsAPI
@@ -124,24 +124,24 @@ class TestOpenFoodFactsAPI(TestCase):
124
124
125
125
```
126
126
127
-
Il y déja quelques informations avec la structure de test présentée au-dessus. En effet, qu'est-ce qui se passe déjà?
127
+
Il y déja quelques informations avec la structure de test présentée au-dessus. En effet, que se passe-t-il déjà?
128
128
1. On importe dans une variable locale la classe **OpenFoodFactsAPI** afin de pouvoir l'utiliser dans notre test
129
129
2. On importe dans une variable locale la classe **TestCase** du module unittest dont notre classe de test va hériter afin de pouvoir notamment utiliser les méthodes de comparaison
130
-
3. On définit une méthode de test où:
130
+
3. On définit une méthode de test où:
131
131
* on instancie un objet de la classe OpenFoodFactsAPI
132
132
* on définie un résultat (ici 2)
133
133
* on utilise la méthode assertEqual de la classe TestCase pour comparer le résultat renvoyé par la méthode **count_product_numb()** avec le résultat que l'on attend
134
134
135
-
Ne criez pas au scandale! J'entend d'ici votre question. Pourquoi 2 comme résultat? Comment sait-on que l'API va renvoyez deux produits avec une bonne note alimentaire?
136
-
Et bien oui, on ne le sait pas... Vous voyez un peu l'impasse? Pourtant, cette méthode doit absolument être testé car ce qu'elle renvoie sera utilisé à l'extérieur de la classe et il est donc important que cette dernière fasse bien le travail qui lui a été demandé.
135
+
Ne criez pas au scandale! J'entend d'ici votre question. Pourquoi 2 comme résultat? Comment sait-on que l'API va nous renvoyer deux produits avec une bonne note alimentaire?
136
+
Et bien oui, on ne le sait pas... Vous voyez un peu l'impasse? Pourtant, cette méthode doit absolument être testée car ce qu'elle renvoie sera utilisée à l'extérieur de la classe et il est donc important que cette dernière fasse bien le travail qui lui a été demandé.
137
137
138
-
Comment faire alors?... Il faut que je puisse tester cette méthode en lui donnant en entrée un json similaire à ce qui est renvoyé par la méthode **_get_product_from_api()** comportant donc 2 produits avec une bonne note alimentaire. (J'aurais bien évidemment pu en choisir 3, 5 ou 10000). Voyons dans la suite de l'article comment faire.
138
+
Comment faire alors?... Il faut que je puisse tester cette méthode en lui donnant en entrée un json similaire à ce qui est renvoyé par la méthode **_get_product_from_api()** comportant donc 2 produits avec une bonne note alimentaire. (J'aurais bien évidemment pu en choisir 3, 5 ou 10000). Voyons dans la suite de l'article comment faire.
139
139
140
140
### La solution sans utilisation d'un mock
141
141
142
-
Comment??? On parle d'un sujet sur les mocks et on propose une solution sans mock? Oui, je l'admets, c'est un peu culotté de ma part mais je pense que ce n'est pas inutile de la décrire car ça aide à comprendre un peu la logique de fonctionnement.
142
+
Comment??? On parle d'un sujet sur les mocks et on propose une solution sans mock? Oui, je l'admets, c'est un peu culotté de ma part mais je pense que ce n'est pas inutile de la décrire car ça aide à comprendre un peu la logique de fonctionnement.
143
143
144
-
Regardons déjà la solution:
144
+
Regardons déjà la solution:
145
145
146
146
```python
147
147
from app import OpenFoodFactsAPI
@@ -198,16 +198,16 @@ class TestOpenFoodFactsAPI(TestCase):
Et voilà le travail! On utilise ici le caractère dynamique de Python ainsi que ces règle de portées pour "forcer" le retour de la méthode **_get_product_from_api**. De cette façon, la méthode **count_product_numb()** va utiliser le dictionnaire défini et retourné par la méthode **fake_api_result()** pour compter le nombre de produit ayant une bonne note.
201
+
Et voilà le travail! On utilise ici le caractère dynamique de Python ainsi que ces règle de portées de variables pour "forcer" le retour de la méthode **_get_product_from_api**. De cette façon, la méthode **count_product_numb()** va utiliser le dictionnaire défini et retourné par la méthode **fake_api_result()** pour compter le nombre de produit ayant une bonne note.
202
202
203
-
Alors c'est super et ça fonctionne mais imaginons maintenant que l'on doit mocker plusieurs éléments, cela risque de rendre le code difficilement lisible et il doit y avoir une méthode plus sympa que de faire des méthodes dans des méthodes.
203
+
Alors c'est super et ça fonctionne mais imaginons maintenant que l'on ait à mocker plusieurs éléments, cela risque de rendre le code difficilement lisible et il doit y avoir une méthode plus sympa que de créer des méthodes imbriquées.
204
204
Et bien oui, c'est le cas. Voyons maintenant deux autres façons de faire en utilisant l'object **Mock** du module unittest, puis de son décorateur **patch**.
205
205
206
206
### Utilisation de la classe Mock
207
207
208
-
Unittest propose une classe Mock permettant de "mocker" facilement une classe, un objet ou une méthode. Cela permet d'indiquer au processus de test que cet objet est une imitation et on va pouvoir agir dessus par l'intermédiaire des méthodes de la classe Mock (comme par exemple lui retourner une valeur!).
208
+
Unittest propose une classe Mock permettant de "mocker" facilement une classe, un objet ou une méthode. Cela permet d'indiquer au processus de test que cet objet est une imitation et on va pouvoir agir dessus par l'intermédiaire des méthodes de la classe Mock (comme par exemple lui retourner une valeur!).
209
209
210
-
Voyons ce que cela donne:
210
+
Voyons ce que cela donne:
211
211
212
212
```python
213
213
from app import OpenFoodFactsAPI
@@ -258,14 +258,14 @@ class TestOpenFoodFactsAPI(TestCase):
Une des autres façons de mettre en place un mock avec unittest est d'utiliser son décorateur patch.
277
277
Ce décorateur permet - comme son nom l'indique - de 'patcher' un objet uniquement au sein de la fonction à laquelle elle est appelée. En effet, cela gère automatiquement le 'dé-patching' même si des exceptions sont levées.
278
278
279
-
Voyons un peu ce que cela donne:
279
+
Voyons un peu ce que cela donne:
280
280
281
281
```python
282
282
from app import OpenFoodFactsAPI
@@ -325,7 +325,7 @@ class TestOpenFoodFactsAPI(TestCase):
1. On importe le décorateur patch du module unittest
330
330
2. On met en place le décorateur qui prend en argument l'objet à mocker
331
331
3. Le décorateur injecte l'objet mocker au sein de la fonction comme un argument de la méthode. Le nom de l'argument est libre de choix. Ici, il s'agit de l'argument **mock_get_product_from_api**.
@@ -334,7 +334,7 @@ Décrivons également les différentes étapes:
334
334
6. On utilise la méthode assertEqual de la classe TestCase pour comparer la valeur renvoyée par la méthode que l'on teste avec le résultat attendu
335
335
336
336
Petite remarque supplémentaire, attention à ne patcher uniquement que la méthode que l'on souhaite mocker et non la classe entière.
337
-
En effet, en faisant comme ceci:
337
+
En effet, en faisant comme ceci:
338
338
339
339
```python
340
340
...
@@ -354,20 +354,20 @@ Du coup, lors du lancement du test, l'appel à l'API sera réalisé et la métho
354
354
355
355
Attention donc à bien mocker le périmètre que l'on souhaite imiter.
356
356
357
-
## Quelle solution choisir au final?
357
+
## Quelle solution choisir au final?
358
358
359
-
Et bien, de mon côté, j'ai une préférence pour l'utilisation du décorateur **@patch** car je trouve que le code est plus lisible et cela m'assure surtout que le mock ne sera actif que durant le test de ma méthode car comme indiqué plus haut, il gère automatiquement la destruction du mock à la fin du test.
359
+
Et bien, de mon côté, j'ai une préférence pour l'utilisation du décorateur **patch** car je trouve que le code est plus lisible et cela m'assure surtout que le mock ne sera actif que durant le test de ma méthode car comme indiqué plus haut, il gère automatiquement la destruction du mock à la fin du test.
360
360
En revanche, l'utilisation de la classe Mock n'est pas dénué d'intérêt lorsque plusieurs tests (donc plusieurs méthodes) doivent faire appel au même objet à mocker. J'aurais plus tendance à utiliser cette solution dans ce type de cas en définissant mon objet mockée dans une méthode *SetUp*.
361
361
362
-
Mais au final, il n'appartient qu'à vous de choisir. Les goûts et les couleurs, ça ne se discute pas!
362
+
Mais au final, il n'appartient qu'à vous de choisir. Les goûts et les couleurs, ça ne se discute pas!
363
363
364
364
## Pour faire quelques tests
365
365
366
-
Si vous souhaitez faire quelques tests sur l'exemple de l'article, vous pouvez retrouver le code source à cette adresse:
366
+
Si vous souhaitez faire quelques tests sur l'exemple de l'article, vous pouvez retrouver le code source à cette adresse:
367
367
<https://github.com/JN-Lab/Test-Mock-Unittest>
368
368
369
369
Il y a différents fichiers de tests avec différentes méthodes appliquées dont une qui ne fonctionnent pas et qui faitréférence au danger expliqué plus haut.
370
370
371
371
J'espère en tout cas que cet article vous aura permis d'y voir un peu plus clair sur la façon de mettre en place des mocks avec le module unittest.
0 commit comments