119 votes

Puis-je patcher un décorateur Python avant qu'il n'enveloppe une fonction ?

J'ai une fonction avec un décorateur que j'essaie de tester à l'aide de l'outil Python Faux bibliothèque. J'aimerais utiliser mock.patch pour remplacer le décorateur réel par un décorateur fictif "bypass" qui appelle simplement la fonction.

Ce que je n'arrive pas à comprendre, c'est comment appliquer le patch avant que le vrai décorateur n'enveloppe la fonction. J'ai essayé plusieurs variantes de la cible du correctif et de réorganiser les déclarations de correctif et d'importation, mais sans succès. Des idées ?

86voto

user2859458 Points 650

Il convient de noter que plusieurs des réponses données ici corrigeront le décorateur pour l'ensemble de la session de test plutôt que pour une seule instance de test, ce qui peut s'avérer indésirable. Voici comment patcher un décorateur qui ne persiste que pour 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 des 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

Lorsque notre test est collecté au cours d'une exécution de 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 nous en débarrasser, 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 callback de nettoyage, kill_patches, restaure le décorateur d'origine et le réapplique à l'unité que nous étions en train de tester. De cette façon, notre patch ne persiste que pour un seul test et non pour toute la session, ce qui est exactement la façon dont tout autre patch devrait se comporter. De plus, puisque le nettoyage appelle patch.stopall(), nous pouvons lancer 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 la façon dont le rechargement affectera les choses. Si un module prend trop de temps ou a une logique qui s'exécute à l'importation, il se peut que vous deviez simplement hausser les épaules et tester le décorateur dans le cadre de l'unité :( Espérons que votre code est mieux écrit que cela. Pas vrai ?

Si l'on ne se soucie pas de savoir si le correctif est appliqué à l'ensemble de la session de test La façon la plus simple de le faire est de le faire au début 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

Veillez à patcher le fichier avec le décorateur plutôt qu'avec la portée locale de l'UUT et à lancer le patch avant d'importer l'unité avec le décorateur.

Il est intéressant de noter que 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é. Il faut savoir que cette méthode appliquera un correctif à tous les autres fichiers du test qui seront importés par la suite, même s'ils ne déclarent pas de correctif eux-mêmes.

67voto

kindall Points 60645

Les décorateurs sont appliqués au moment de la définition de la fonction. Pour la plupart des fonctions, il s'agit du chargement du module. (Les fonctions définies dans d'autres fonctions se voient appliquer le décorateur chaque fois que la fonction qui les englobe est appelée).

Ainsi, si vous souhaitez modifier un décorateur, vous devez procéder comme suit :

  1. Importer le module qui le contient
  2. Définir la fonction décorative fictive
  3. Set (jeu de mots) par exemple module.decorator = mymockdecorator
  4. Importer le(s) module(s) qui utilise(nt) le décorateur, ou l'utiliser 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.

Modification pour refléter les changements apportés à Python depuis que j'ai écrit ce texte : Si le décorateur utilise functools.wraps() et que la version de Python est suffisamment récente, vous pourrez peut-être retrouver la fonction originale en utilisant la fonction __wrapped__ et le redécorer, mais cela n'est en aucun cas garanti, et le décorateur que vous souhaitez remplacer n'est pas forcément le seul décorateur appliqué.

22voto

user7815681 Points 151

Lorsque j'ai rencontré ce problème pour la première fois, je me suis creusé la tête pendant des heures. J'ai trouvé un moyen beaucoup plus simple de résoudre ce problème.

Cela permet de contourner complètement le décorateur, comme si la cible n'avait pas été décorée au départ.

Il est divisé en deux parties. Je vous suggère de lire l'article suivant.

http://alexmarandon.com/articles/python_mock_gotchas/

Deux problèmes que je n'ai cessé de rencontrer :

1.) Mockez 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 faites pas de mock avant l'importation, le module ne tiendra pas compte du mock. Après le chargement, vous devez faire un étrange mock.patch.object, ce qui devient encore plus frustrant.

2.) Assurez-vous que le chemin d'accès au décorateur est correct.

Rappelez-vous que le patch du décorateur que vous mockez 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 les chemins complets pour les importations. Cela rend les choses beaucoup plus faciles pour les tests.

Les étapes :

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 intérieur avec.

with mock.patch('path.to.my.decorator', mock_decorator):
     from mymodule import myfunction

2b.) Patch en tête de fichier, ou dans TestCase.setUp

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 cas de test ou dans ses méthodes/cas de test.

from mymodule import myfunction

2.) Utiliser 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 simuler chaque décorateur séparément, alors faites attention à ceux que vous manquez.

4voto

InbalZelig Points 11

Nous avons essayé de simuler un décorateur qui reçoit parfois un autre paramètre comme une chaîne de caractères, et parfois non, par ex :

@myDecorator('my-str')
def function()

OR

@myDecorator
def function()

Grâce à l'une des réponses ci-dessus, nous avons écrit une fonction fictive et patché le décorateur avec cette fonction fictive :

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, mais qui fait seulement quelques 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 a donc besoin de transférer les paramètres de la fonction, la fonction mock_decorator doit être un peu différente.

J'espère que cela aidera d'autres personnes...

2voto

Eric Mintz Points 21

La méthode suivante a fonctionné pour moi :

  1. Éliminez l'instruction d'importation qui charge la cible de test.
  2. Patch du décorateur au démarrage du test comme appliqué ci-dessus.
  3. Invoquer importlib.import_module() immédiatement après Parcheando pour charger la cible de test.
  4. Exécuter les tests normalement.

Il a fonctionné comme un charme.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X