Puis-je patcher un décorateur Python avant d'envelopper une fonction?

J'ai une fonction avec un décorateur que j'essaie de tester à l'aide de la bibliothèque Python Mock. J'aimerais utiliser la maquette.patch pour remplacer le vrai décorateur par un décorateur "bypass" qui appelle simplement la fonction. Ce que je ne peux pas comprendre, c'est comment appliquer le patch avant que le vrai décorateur enveloppe la fonction. J'ai essayé quelques variations différentes sur la cible de patch et réorganiser les instructions de patch et d'importation, mais sans succès. Des idées?

48
demandé sur Chris Sears 2011-10-06 00:50:20

7 réponses

Les décorateurs sont appliqués au moment de la définition de la fonction. Pour la plupart des fonctions, c'est lorsque le module est chargé. (Les fonctions qui sont définies dans d'autres fonctions ont le décorateur appliqué chaque fois que la fonction englobante est appelée.)

Donc, si vous voulez singe-patch un décorateur, ce que vous devez faire est:

  1. importez le module qui le contient
  2. définir la fonction de décorateur fictif
  3. définir par exemple module.decorator = mymockdecorator
  4. importez le(S) module (s) qui utilise le décorateur, ou utilisez-le dans votre propre module

Si le module qui contient le décorateur contient également des fonctions qui l'utilisent, celles-ci sont déjà décorées au moment où vous pouvez les voir, et vous êtes probablement S. O. L.

Modifier pour refléter les modifications apportées à Python depuis que j'ai écrit ceci à l'origine: si le décorateur utilise functools.wraps() et que la version de Python est assez nouvelle, vous pourrez peut-être extraire la fonction d'origine en utilisant l'attritube __wrapped__ et la décorer, mais ce n'est en aucun cas garanti, et le décorateur que vous souhaitez remplacer peut également ne pas être le seul décorateur appliqué.

39
répondu kindall 2016-01-28 17:44:08

Il convient de noter que plusieurs des réponses ici vont patcher 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 patcher un décorateur qui ne persiste que par 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é testée (parce que 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 l'applique de nouveau à l'unité que nous testions. De cette façon, notre patch ne persiste que via un seul test plutôt que toute la session-ce qui est exactement la façon dont tout autre patch devrait se comporter. En outre, depuis le nettoyage appelle patch.stopall (), nous pouvons commencer tous les autres correctifs dans la configuration() 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 à l'importation, vous devrez peut-être hausser les épaules et tester le décorateur dans le cadre de l'unité. : (Espérons que votre code est mieux écrit que cela. Droit?

Si on ne se soucie pas si le patch est appliqué à toute la session de test , le moyen le plus simple de le faire est 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 commencer le patch avant d'importer l'unité avec le décorateur.

Fait intéressant, même si le patch est arrêté, tous les fichiers déjà importés auront toujours le patch appliqué au Décorateur, ce qui est l'inverse de la situation avec laquelle nous avons commencé. Soyez conscient que cela méthode va Patcher tous les autres fichiers dans l'exécution du test qui sont importés par la suite-même s'ils ne déclarent pas un patch eux-mêmes.

26
répondu user2859458 2016-09-02 15:21:30

Quand j'ai rencontré ce problème pour la première fois, j'utilise mon cerveau pendant des heures. J'ai trouvé un moyen beaucoup plus facile 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.

Ceci est divisé en deux parties. Je suggère la lecture de l'article suivant.

Http://alexmarandon.com/articles/python_mock_gotchas/

Deux pièges que je n'arrêtais pas de rencontrer:

1.) Mock le 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 simulation. Après le chargement, vous devez faire une simulation étrange.patch.objet, ce qui devient encore plus frustrant.

2.) Assurez-vous que vous vous moquez du chemin correct vers le décorateur.

Rappelez-vous que le patch du décorateur que vous moquez est basé sur la façon dont votre module charge le décorateur, pas sur la façon dont votre test charge le décorateur. Ce c'est pourquoi je suggère toujours d'utiliser des chemins complets pour les importations. Cela rend les choses beaucoup plus faciles pour les tests.

Étapes:

1.) La fonction simulée:

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.configuration

mock.patch('path.to.my.decorator', mock_decorator).start()

L'une ou l'autre de ces méthodes vous permettra d'importer votre fonction à tout moment dans le TestCase ou ses cas de méthode/test.

from mymodule import myfunction

2.) Utiliser une fonction distincte comme un effet secondaire de maquette.patch.

Maintenant, vous pouvez utiliser mock_decorator pour chaque décorateur que vous voulez moquer. Vous devrez vous moquer de chaque décorateur séparément, alors faites attention à ceux que vous manquez.

2
répondu user7815681 2018-05-22 15:15:41

Peut-être que vous pouvez 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.

0
répondu Aditya Mukherji 2011-10-05 21:04:25

Ce qui suit a fonctionné pour moi:

  1. supprime l'instruction import qui charge la cible de test.
  2. patcher le décorateur au démarrage du test comme appliqué ci-dessus.
  3. appelez importlib.import_module () immédiatement après le correctif pour charger la cible de test.
  4. Exécutez les tests normalement.

Ça a marché comme un charme.

0
répondu Eric Mintz 2014-01-17 15:49:18

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 de test. (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 que la fonction décorée réside dans present, vous corrigeriez pretty.decorator en utilisant la machine fictive et remplaceriez MODULE par present. Quelque chose comme ce qui suit devrait fonctionner (non testé).

Classe TestDecorator (unittest.Cas de test) : ...

  @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 courant sys.path du module de test. Cette copie est faite lorsque le module est d'abord analysé en assurant un sys.path cohérent pour tous les test.

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 globalement casse 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 disponible une importation particulière du module pour toutes les fonctions de test de la classe.

Intégré

Ceux qui jouent avec les importations builtin 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 le drapeau approprié(Voir python --help). Ceux-ci peuvent ensuite être importés localement en utilisant import builtins, IIRC.

0
répondu Carel 2018-05-12 23:10:47

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 params, vous devriez:

def MockAuthenticated(func):
    return func

From tornado import web web.authenticated = MockAuthenticated

-1
répondu guochunyang 2015-11-20 08:05:26