J'ai une fonction avec un décorateur que j'essaie de tester avec l'aide du Mock bibliothèque. Je voudrais utiliser mock.patch pour remplacer le vrai décorateur par un faux décorateur de contournement qui appelle simplement la fonction. Ce que je ne peux pas comprendre, c'est comment appliquer le patch avant que le vrai décorateur n'encapsule la fonction. J'ai essayé quelques variations différentes sur la cible du patch et réorganisé les instructions de patch et d'importation, mais sans succès. Des idées?
8 réponses
Pour @lru_cache (max_size = 1000)
class MockedLruCache(object):
def __init__(self, maxsize=0, timeout=0): pass def __call__(self, func): return func
cache.LruCache = MockedLruCache
Si vous utilisez un décorateur qui n'a pas de paramètres, vous devez:
def MockAuthenticated(func):
return func
from tornado import web
web.authenticated = MockAuthenticated
Nous avons essayé de nous moquer d'un décorateur qui obtient parfois un autre paramètre comme une chaîne, et parfois non, par exemple:
@myDecorator('my-str')
def function()
OR
@myDecorator
def function()
Grâce à l'une des réponses ci-dessus, nous avons écrit une fonction de simulation et patché le décorateur avec cette fonction de simulation:
from mock import patch
def mock_decorator(f):
def decorated_function(g):
return g
if callable(f): # if no other parameter, just return the decorated function
return decorated_function(f)
return decorated_function # if there is a parametr (eg. string), ignore it and return the decorated function
patch('path.to.myDecorator', mock_decorator).start()
from mymodule import myfunction
Notez que cet exemple est bon pour un décorateur qui n'exécute pas la fonction décorée, ne faites que des choses avant l'exécution réelle. Dans le cas où le décorateur exécute également la fonction décorée et qu'il doit donc transférer les paramètres de la fonction, la fonction mock_decorator doit être un peu différente.
J'espère que cela aidera les autres ...
Concept
Cela peut sembler un peu étrange mais on peut patcher sys.path
, avec une copie de lui-même, et effectuer une importation dans le cadre de la fonction de test. Le code suivant montre le concept.
from unittest.mock import patch
import sys
@patch('sys.modules', sys.modules.copy())
def testImport():
oldkeys = set(sys.modules.keys())
import MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys))
oldkeys = set(sys.modules.keys())
testImport() -> ("MODULE") # Set contains MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys)) -> set() # An empty set
MODULE
peut alors être remplacé par le module que vous testez. (Cela fonctionne en Python 3.6 avec MODULE
substitué par xml
par exemple)
OP
Pour votre cas, disons que la fonction décoratrice réside dans le module pretty
et la fonction décorée réside dans present
, alors vous patcheriez pretty.decorator
en utilisant la machinerie fictive et substitueriez MODULE
avec present
. Quelque chose comme ce qui suit devrait fonctionner (non testé).
Classe TestDecorator (unittest.TestCase): ...
@patch(`pretty.decorator`, decorator)
@patch(`sys.path`, sys.path.copy())
def testFunction(self, decorator) :
import present
...
Explication
Cela fonctionne en fournissant un sys.path
"propre" pour chaque fonction de test, en utilisant une copie du sys.path
actuel du module de test. Cette copie est effectuée lors de la première analyse du module, garantissant un sys.path
cohérent pour tous les tests.
Nuances
Il y a cependant quelques implications. Si le framework de test exécute plusieurs modules de test sous la même session python, tout module de test qui importe MODULE
interrompt globalement tout module de test qui l'importe localement. Cela oblige à effectuer l'importation localement partout. Si le framework exécute chaque module de test sous une session python distincte, cela devrait fonctionner. De même, vous ne pouvez pas importer MODULE
globalement dans un module de test où vous importez MODULE
localement.
Les importations locales doivent être effectuées pour chaque fonction de test dans une sous-classe de unittest.TestCase
. Il est peut-être possible de l'appliquer à la sous-classe unittest.TestCase
en rendant directement une importation particulière du module disponible pour toutes les fonctions de test de la classe.
Ins intégré
Ceux qui jouent avec builtin
les importations trouveront le remplacement de MODULE
par sys
, os
, etc. échouera, car ceux-ci sont déjà lus sur sys.path
lorsque vous essayez de le copier . L'astuce ici est d'invoquer Python avec les importations intégrées désactivées, je pense que python -X test.py
le fera mais j'oublie l'indicateur approprié (Voir python --help
). Ceux-ci peuvent ensuite être importés localement à l'aide de import builtins
, IIRC.
Ce qui suit a fonctionné pour moi:
- Éliminez l'instruction d'importation qui charge la cible de test.
- Patcher le décorateur au démarrage du test comme appliqué ci-dessus.
- Appelez importlib.import_module () immédiatement après l'application de correctifs pour charger la cible de test.
- Exécutez les tests normalement.
Ça a marché comme sur des roulettes.
Vous pouvez peut-être appliquer un autre décorateur sur les définitions de tous vos décorateurs qui vérifie essentiellement une variable de configuration pour voir si le mode de test est destiné à être utilisé.
Si oui, il remplace le décorateur qu'il décore par un décorateur factice qui ne fait rien.
Sinon, il laisse passer ce décorateur.
Lorsque j'ai rencontré ce problème pour la première fois, j'utilisais mon cerveau pendant des heures. J'ai trouvé un moyen beaucoup plus simple de gérer cela.
Cela contournera complètement le décorateur, comme si la cible n'était même pas décorée en premier lieu.
Cela se décompose en deux parties. Je suggère de lire l'article suivant.
http://alexmarandon.com/articles/python_mock_gotchas/
Deux Gotchas que je continuais à rencontrer:
1.) Se moquer du décorateur avant l'importation de votre fonction / module.
Les décorateurs et les fonctions sont définis au moment du chargement du module. Si vous ne vous moquez pas avant l'importation, cela ne tiendra pas compte de la maquette. Après le chargement, vous devez faire un mock.patch.object bizarre, ce qui devient encore plus frustrant.
2.) Assurez-vous de vous moquer du bon chemin vers le décorateur.
N'oubliez pas que le patch du décorateur dont vous vous moquez est basé sur la façon dont votre module charge le décorateur, et non sur la façon dont votre test charge le décorateur. C'est pourquoi je suggère de toujours utiliser des chemins d'accès complets pour les importations. Cela rend les choses beaucoup plus faciles à tester.
Pas:
1.) La fonction Mock:
from functools import wraps
def mock_decorator(*args, **kwargs):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return f(*args, **kwargs)
return decorated_function
return decorator
2.) Se moquer du décorateur:
2a.) Chemin à l'intérieur avec.
with mock.patch('path.to.my.decorator', mock_decorator):
from mymodule import myfunction
2b.) Patch en haut du fichier ou dans TestCase.setUp
mock.patch('path.to.my.decorator', mock_decorator).start()
L'une ou l'autre de ces façons vous permettra d'importer votre fonction à tout moment dans TestCase ou ses méthodes / cas de test.
from mymodule import myfunction
2.) Utilisez une fonction distincte comme effet secondaire du mock.patch.
Vous pouvez maintenant utiliser mock_decorator pour chaque décorateur que vous souhaitez simuler. Vous devrez vous moquer de chaque décorateur séparément, alors faites attention à ceux que vous manquez.
Pour patcher un décorateur, vous devez importer ou recharger le module qui utilise ce décorateur après le patcher OU redéfinir la référence du module à ce décorateur.
Les décorateurs sont appliqués au moment de l'importation d'un module. C'est pourquoi si vous avez importé un module qui utilise un décorateur que vous souhaitez patcher en haut de votre fichier et essayer de le patcher plus tard sans le recharger, le patch n'aurait aucun effet.
Voici un exemple de la première façon mentionnée de le faire - recharger un module après avoir patché un décorateur qu'il utilise:
import moduleA
...
# 1. patch the decorator
@patch('decoratorWhichIsUsedInModuleA', examplePatchValue)
def setUp(self)
# 2. reload the module which uses the decorator
reload(moduleA)
def testFunctionA(self):
# 3. tests...
assert(moduleA.functionA()...
Références utiles:
- Documentation Python 3 pour
imp.reload
- Documentation Python 2.7 pour
reload
- http://alexmarandon.com/articles/python_mock_gotchas/#patching-decorators
Il convient de noter que plusieurs des réponses ici patcheront le décorateur pour toute la session de test plutôt qu'une seule instance de test; ce qui peut être indésirable. Voici comment corriger un décorateur qui ne persiste qu'à travers un seul test.
Notre unité à tester avec le décorateur indésirable:
# app/uut.py
from app.decorators import func_decor
@func_decor
def unit_to_be_tested():
# Do stuff
pass
Du module décorateurs:
# app/decorators.py
def func_decor(func):
def inner(*args, **kwargs):
print "Do stuff we don't want in our test"
return func(*args, **kwargs)
return inner
Au moment où notre test est collecté lors d'un test, le décorateur indésirable a déjà été appliqué à notre unité sous test (car cela se produit au moment de l'importation). Afin de se débarrasser de cela, nous devrons remplacer manuellement le décorateur dans le module du décorateur, puis réimporter le module contenant notre UUT.
Notre module de test:
# test_uut.py
from unittest import TestCase
from app import uut # Module with our thing to test
from app import decorators # Module with the decorator we need to replace
import imp # Library to help us reload our UUT module
from mock import patch
class TestUUT(TestCase):
def setUp(self):
# Do cleanup first so it is ready if an exception is raised
def kill_patches(): # Create a cleanup callback that undoes our patches
patch.stopall() # Stops all patches started with start()
imp.reload(uut) # Reload our UUT module which restores the original decorator
self.addCleanup(kill_patches) # We want to make sure this is run so we do this in addCleanup instead of tearDown
# Now patch the decorator where the decorator is being imported from
patch('app.decorators.func_decor', lambda x: x).start() # The lambda makes our decorator into a pass-thru. Also, don't forget to call start()
# HINT: if you're patching a decor with params use something like:
# lambda *x, **y: lambda f: f
imp.reload(uut) # Reloads the uut.py module which applies our patched decorator
Le rappel de nettoyage, kill_patches, restaure le décorateur d'origine et le réapplique à l'unité que nous testions. De cette façon, notre correctif ne persiste que pendant un seul test plutôt que pendant toute la session - c'est exactement la façon dont tout autre correctif devrait se comporter. De plus, puisque le nettoyage appelle patch.stopall (), nous pouvons démarrer tous les autres patchs dans le setUp () dont nous avons besoin et ils seront nettoyés en un seul endroit.
La chose importante à comprendre à propos de cette méthode est de savoir comment le rechargement affectera les choses. Si un module prend trop de temps ou a une logique qui s'exécute lors de l'importation, vous devrez peut-être simplement hausser les épaules et tester le décorateur dans le cadre de l'unité. :( J'espère que votre code est mieux écrit que ça.
Si l'on ne se soucie pas si le patch est appliqué à toute la session de test , la façon la plus simple de le faire est juste en haut du fichier de test:
# test_uut.py
from mock import patch
patch('app.decorators.func_decor', lambda x: x).start() # MUST BE BEFORE THE UUT GETS IMPORTED ANYWHERE!
from app import uut
Assurez-vous de patcher le fichier avec le décorateur plutôt que la portée locale de l'UUT et de démarrer le patch avant d'importer l'unité avec le décorateur.
Fait intéressant, même si le correctif est arrêté, tous les fichiers déjà importés auront toujours le correctif appliqué au décorateur, ce qui est l'inverse de la situation avec laquelle nous avons commencé. N'oubliez pas que cette méthode corrigera tous les autres fichiers du test qui seront importés par la suite - même s'ils ne déclarent pas eux-mêmes un correctif.
Questions connexes
De nouvelles questions
python
Python est un langage de programmation multi-paradigme, typé dynamiquement et polyvalent. Il est conçu pour être rapide à apprendre, comprendre, utiliser et appliquer une syntaxe propre et uniforme. Veuillez noter que Python 2 est officiellement hors support à partir du 01-01-2020. Néanmoins, pour les questions Python spécifiques à la version, ajoutez la balise [python-2.7] ou [python-3.x]. Lorsque vous utilisez une variante Python (par exemple, Jython, PyPy) ou une bibliothèque (par exemple, Pandas et NumPy), veuillez l'inclure dans les balises.