comment exécuter des Tests unitaires en Python sans tester votre Patience
Le plus souvent, le logiciel que nous écrivons interagit directement avec ce que nous qualifierions de services « sales”. En termes simples: des services qui sont cruciaux pour notre application, mais dont les interactions ont des effets secondaires intentionnels mais indésirables-c’est-à-dire indésirables dans le cadre d’un test autonome.,
Par exemple: peut-être que nous sommes en train d’écrire une application sociale et que vous souhaitez tester notre nouveau ‘Post Facebook en vedette », mais vous ne voulez pas fait de post de Facebook à chaque fois que nous courons à notre suite de test.
la bibliothèque Pythonunittest
comprend un sous—paquet nomméunittest.mock
—ou si vous le déclarez comme une dépendance, simplementmock
-qui fournit des moyens extrêmement puissants et utiles pour se moquer et éliminer ces effets secondaires indésirables.,
Note: mock
est nouvellement inclus dans la bibliothèque standard de Python 3.3; les distributions a priori aurez à utiliser la Maquette de la bibliothèque téléchargeable via PyPI.
appels système vs. Python Mocking
pour vous donner un autre exemple, et celui que nous allons exécuter pour le reste de l’article, considérez les appels système., Il n’est pas difficile de voir que ce sont des candidats de choix pour se moquer: que vous écriviez un script pour éjecter un lecteur de CD, un serveur web qui supprime les fichiers de cache obsolètes De /tmp
, ou un serveur socket qui se lie à un port TCP, ces appels présentent tous des effets
en tant que développeur, vous vous souciez plus que votre bibliothèque a appelé avec succès la fonction système pour éjecter un CD (avec les arguments corrects, etc.) par opposition à l’expérience réelle de votre plateau de CD ouvert à chaque fois qu’un test est exécuté. (Ou pire, plusieurs fois, car plusieurs tests référencent le code d’éjection lors d’une seule exécution de test unitaire!)
de même, garder vos tests unitaires efficaces et performants signifie garder autant de « code lent” hors des essais automatisés, à savoir l’accès au système de fichiers et au réseau.,
pour notre premier exemple, nous allons refactoriser un cas de test Python standard de la forme originale en un en utilisant mock
. Nous allons démontrer comment l’écriture d’un cas de test avec mocks rendra nos tests plus intelligents, plus rapides et capables d’en révéler plus sur le fonctionnement du logiciel.
une simple fonction de suppression
Nous avons tous besoin de supprimer des fichiers de notre système de fichiers de temps en temps, alors écrivons une fonction en Python qui facilitera un peu la tâche de nos scripts.,
#!/usr/bin/env python# -*- coding: utf-8 -*-import osdef rm(filename): os.remove(filename)
évidemment, notre méthoderm
à ce stade, ne fournit pas beaucoup plus que la méthodeos.remove
sous-jacente, mais notre base de code s’améliorera, nous permettant d’ajouter plus de fonctionnalités ici.
écrivons un cas de test traditionnel, c’est-à-dire sans moqueries:
notre cas de test est assez simple, mais chaque fois qu’il est exécuté, un fichier temporaire est créé puis supprimé., De plus, nous n’avons aucun moyen de tester si notre méthode rm
transmet correctement l’argument à l’appel os.remove
. Nous pouvons supposer que c’est le cas sur la base du test ci-dessus, mais beaucoup reste à désirer.
Refactoring avec Python Mocks
refactorisons notre cas de test en utilisantmock
:
avec ces refactors, nous avons fondamentalement changé la façon dont le test fonctionne. Maintenant, nous avons un initié, un objet que nous pouvons utiliser pour vérifier la fonctionnalité de l’autre.,
pièges potentiels de moquerie Python
l’une des premières choses qui devraient ressortir est que nous utilisons le décorateur de méthodemock.patch
pour simuler un objet situé àmymodule.os
, et injectons cette simulation dans notre méthode de cas de test. N’aurait – il pas plus de sens de simplement se moquer de os
lui-même, plutôt que de la référence à mymodule.os
?
Eh bien, Python est un peu un serpent sournois en ce qui concerne les importations et la gestion des modules., Au moment de l’exécution, le module mymodule
a son propre os
qui est importé dans sa propre portée locale dans le module. Ainsi, si nous nous moquons de os
, nous ne verrons pas les effets de la simulation dans le module mymodule
.
Le mantra à répéter est ceci:
se Moquer un article où il est utilisé, non pas d’où il vient.,
Si vous avez besoin de se moquer de l’ tempfile
module myproject.app.MyElaborateClass
, vous avez probablement besoin d’appliquer la maquette de myproject.app.tempfile
, comme chaque module conserve ses propres importations.
avec cet écueil à l’écart, continuons à nous moquer.
ajouter une Validation à ‘rm’
la méthoderm
définie précédemment est assez simplifiée. Nous aimerions qu’il valide qu’un chemin existe et qu’il s’agit d’un fichier avant de tenter aveuglément de le supprimer., Nous allons refactoriser le code rm
pour être un peu plus intelligent:
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport os.pathdef rm(filename): if os.path.isfile(filename): os.remove(filename)
la Grande. Maintenant, ajustons notre cas de test pour maintenir la couverture.
notre paradigme de test a complètement changé. Nous pouvons maintenant vérifier et valider la fonctionnalité interne des méthodes sans aucun effet secondaire.
File-Removal as a Service with Mock Patch
Jusqu’à présent, nous n’avons travaillé qu’avec la fourniture de mocks pour les fonctions, mais pas pour les méthodes sur les objets ou les cas où le mocking est nécessaire pour envoyer des paramètres. Couvrons d’abord les méthodes d’objet.,
nous allons commencer par un refactor de la méthoderm
dans une classe de service. Il n’y a vraiment pas de besoin justifiable, en soi, d’encapsuler une fonction aussi simple dans un objet, mais cela nous aidera à tout le moins à démontrer les concepts clés dans mock
. Refactorisons:
vous remarquerez que peu de choses ont changé dans notre cas de test:
génial, donc nous savons maintenant que leRemovalService
fonctionne comme prévu., Créons un autre service qui le déclare comme une dépendance:
puisque nous avons déjà une couverture de test sur laRemovalService
, nous n’allons pas valider la fonctionnalité interne de larm
méthode dans nos tests deUploadService
. Au contraire, nous allons simplement tester (sans effets secondaires, bien sûr) que UploadService
appelle la RemovalService.rm
méthode, que nous savons « just works™” de notre cas de test précédent.
Il y a deux façons d’aller à ce sujet:
- se Moquer de la
RemovalService.rm
méthode elle-même., - fournit une instance moquée dans le constructeur de
UploadService
.
comme les deux méthodes sont souvent importantes dans les tests unitaires, nous examinerons les deux.
Option 1: moquer les méthodes D’Instance
la bibliothèque mock
a un décorateur de méthode spécial pour moquer les méthodes et propriétés d’instance d’objet, le@mock.patch.object
décorateur:
génial! Nous avons validé que la UploadService
appelle avec succès la méthode rm
de notre instance. Remarquez quelque chose d’intéressant là-dedans?, Le mécanisme de correction a en fait remplacé la méthoderm
de toutes les instancesRemovalService
dans notre méthode de test. Cela signifie que nous pouvons réellement inspecter les instances elles-mêmes. Si vous voulez en voir plus, essayez de supprimer un point d’arrêt dans votre code moqueur pour avoir une bonne idée du fonctionnement du mécanisme de patch.
écueil du Patch simulé: Ordre des décorateurs
lorsque vous utilisez plusieurs décorateurs sur vos méthodes de test, l’ordre est important et c’est un peu déroutant. Fondamentalement, lorsque vous mappez des décorateurs sur des paramètres de méthode, travaillez à l’envers., Considérez cet exemple:
remarquez comment nos paramètres sont appariés à l’ordre inverse des décorateurs? C’est en partie à cause de la façon dont Python fonctionne. Avec plusieurs méthode décorateurs, voici l’ordre d’exécution en pseudo-code:
patch_sys(patch_os(patch_os_path(test_something)))
Depuis le patch sys
est ultrapériphériques patch, il sera exécuté dernier, faisant d’elle le dernier paramètre dans la méthode d’essai arguments. Prenez bien en note et utilisez un débogueur lors de l’exécution de vos tests pour vous assurer que les bons paramètres sont injectés dans le bon ordre.,
Option 2: Création d’Instances simulées
Au Lieu de se moquer de la méthode d’instance spécifique, nous pourrions simplement fournir une instance simulée àUploadService
avec son constructeur. Je préfère l’option 1 ci-dessus, car elle est beaucoup plus précise, mais il existe de nombreux cas où l’option 2 peut être efficace ou nécessaire. Refactorisons à nouveau notre test:
dans cet exemple, nous n’avons même pas eu à patcher de fonctionnalité, nous créons simplement une spécification automatique pour la classe RemovalService
, puis injectons cette instance dans notre UploadService
pour valider la fonctionnalité.,
la méthode mock.create_autospec
crée une instance fonctionnellement équivalente à la classe fournie. Ce que cela signifie, en pratique, c’est que lorsque l’instance renvoyée interagit avec, elle déclenchera des exceptions si elle est utilisée de manière illégale. Plus précisément, si une méthode est appelée avec le mauvais nombre d’arguments, une exception sera levée. Ceci est extrêmement important car les refactors se produisent. Comme une bibliothèque change, les tests se brisent et cela est attendu. Sans utiliser une spécification automatique, nos tests passeront toujours même si l’implémentation sous-jacente est cassée.,
Écueil: La balise mock.Mock
et mock.MagicMock
Classes
Le mock
bibliothèque comprend également deux classes pour lesquelles la plupart de fonctionnement repose sur: mock.Mock
et mock.MagicMock
. Lorsque vous avez le choix d’utiliser une instance mock.Mock
, une instance mock.MagicMock
ou une spécification automatique, privilégiez toujours l’utilisation d’une spécification automatique, car cela aide à garder vos tests sains pour les changements futurs., En effet, mock.Mock
Et mock.MagicMock
acceptent tous les appels de méthode et les affectations de propriétés quelle que soit L’API sous-jacente. Considérez le cas d’utilisation suivant:
class Target(object): def apply(value): return valuedef method(target, value): return target.apply(value)
Nous pouvons tester cela avec une instance mock.Mock
comme ceci:
cette logique semble saine, mais modifions la méthode Target.apply
pour prendre plus de paramètres:
class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None
réexécutez votre test, et vous constaterez qu’il passe toujours. C’est parce qu’il n’est pas construit sur votre API réelle., C’est pourquoi vous devriez toujours utiliser la balise create_autospec
méthode et le autospec
paramètre de la balise @patch
et @patch.object
décorateurs.
Python se Moquer Exemple: se moquant d’un Facebook Appel d’API
Pour finir, nous allons écrire une plus applicable dans le monde réel python se moquer exemple, qui nous l’avons mentionné dans l’introduction: l’affichage d’un message à Facebook. Nous allons écrire une belle classe wrapper et un cas de test.,
Voici notre cas de test, qui vérifie que nous publions le message sans le poster réellement:
Comme nous l’avons vu jusqu’à présent, il est vraiment simple de commencer à écrire des tests plus intelligents avecmock
en Python.
Conclusion
la bibliothèquemock
de Python, si elle est un peu déroutante à utiliser, change la donne pour les tests unitaires. Nous avons démontré des cas d’utilisation courants pour commencer à utiliser mock
dans les tests unitaires, et nous espérons que cet article aidera les développeurs Python à surmonter les obstacles initiaux et à écrire un excellent code testé.,