Kann ich einen Python-Dekorator patchen, bevor er eine Funktion umschließt?

83

Ich habe eine Funktion mit einem Dekorateur, die ich mit Hilfe der Python Mock- Bibliothek testen möchte . Ich möchte mock.patchden echten Dekorator durch einen nachgebildeten 'Bypass'-Dekorator ersetzen, der nur die Funktion aufruft.

Was ich nicht herausfinden kann, ist, wie man den Patch anwendet, bevor der echte Dekorateur die Funktion abschließt. Ich habe ein paar verschiedene Variationen des Patch-Ziels ausprobiert und die Patch- und Import-Anweisungen neu angeordnet, aber ohne Erfolg. Irgendwelche Ideen?

Chris Sears
quelle

Antworten:

59

Dekorateure werden zur Zeit der Funktionsdefinition angewendet. Bei den meisten Funktionen wird das Modul geladen. (Bei Funktionen, die in anderen Funktionen definiert sind, wird der Dekorator bei jedem Aufruf der umschließenden Funktion angewendet.)

Wenn Sie also einen Dekorateur mit Affen flicken möchten, müssen Sie Folgendes tun:

  1. Importieren Sie das Modul, das es enthält
  2. Definieren Sie die Mock Decorator-Funktion
  3. Stellen Sie z module.decorator = mymockdecorator
  4. Importieren Sie die Module, die den Dekorator verwenden, oder verwenden Sie sie in Ihrem eigenen Modul

Wenn das Modul, das den Dekorator enthält, auch Funktionen enthält, die ihn verwenden, sind diese bereits dekoriert, wenn Sie sie sehen können, und Sie sind wahrscheinlich SOL

Bearbeiten, um Änderungen an Python widerzuspiegeln, seit ich dies ursprünglich geschrieben habe: Wenn der Dekorator verwendet functools.wraps()und die Version von Python neu genug ist, können Sie möglicherweise die ursprüngliche Funktion mithilfe des __wrapped__Attributs ausgraben und neu dekorieren, dies ist jedoch keinesfalls der Fall garantiert, und der Dekorateur, den Sie ersetzen möchten, ist möglicherweise auch nicht der einzige Dekorateur, der angewendet wird.

irgendwie
quelle
17
Folgendes hat viel Zeit verschwendet: Denken Sie daran, dass Python Module nur einmal importiert. Wenn Sie eine Reihe von Tests ausführen und versuchen, einen Dekorateur in einem Ihrer Tests zu verspotten, und die dekorierte Funktion an eine andere Stelle importiert wird, hat das Verspotten des Dekorateurs keine Auswirkung.
Paragon
2
Verwenden Sie die integrierte reloadFunktion, um den Python-Binärcode docs.python.org/2/library/functions.html#reload neu zu generieren und Ihren Dekorateur zu
monkeypatchen
3
Bin auf das von @Paragon gemeldete Problem gestoßen und habe es umgangen, indem ich meinen Dekorateur in das Testverzeichnis gepatcht habe __init__. Dadurch wurde sichergestellt, dass der Patch vor einer Testdatei geladen wurde. Wir haben einen isolierten Testordner, sodass die Strategie für uns funktioniert, dies funktioniert jedoch möglicherweise nicht für jedes Ordnerlayout.
Claytond
4
Nachdem ich das mehrmals gelesen habe, bin ich immer noch verwirrt. Dies benötigt ein Codebeispiel!
Ritratt
@claytond Danke, deine Lösung hat bei mir funktioniert, da ich einen isolierten Testordner hatte!
Srivathsa
56

Es sollte beachtet werden, dass einige der Antworten hier den Dekorator für die gesamte Testsitzung und nicht für eine einzelne Testinstanz patchen. was unerwünscht sein kann. Hier erfahren Sie, wie Sie einen Dekorateur patchen, der nur durch einen einzigen Test erhalten bleibt.

Unsere Einheit, die mit dem unerwünschten Dekorateur getestet werden soll:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

Vom Dekorateur-Modul:

# 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

Zu dem Zeitpunkt, an dem unser Test während eines Testlaufs erfasst wird, wurde der unerwünschte Dekorateur bereits auf unser zu testendes Gerät angewendet (da dies zum Zeitpunkt des Imports geschieht). Um dies zu beseitigen, müssen wir den Dekorator im Dekoratormodul manuell ersetzen und dann das Modul mit unserem Prüfling erneut importieren.

Unser Testmodul:

#  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

Der Bereinigungsrückruf kill_patches stellt den ursprünglichen Dekorator wieder her und wendet ihn erneut auf das zu testende Gerät an. Auf diese Weise bleibt unser Patch nur durch einen einzigen Test und nicht durch die gesamte Sitzung bestehen - genau so sollte sich jeder andere Patch verhalten. Da die Bereinigung patch.stopall () aufruft, können wir auch alle anderen Patches in setUp () starten, die wir benötigen, und sie werden alle an einem Ort bereinigt.

Das Wichtige an dieser Methode ist, wie sich das Nachladen auf die Dinge auswirkt. Wenn ein Modul zu lange dauert oder eine Logik hat, die beim Import ausgeführt wird, müssen Sie möglicherweise nur den Dekorator als Teil des Geräts zucken und testen. :( Hoffentlich ist dein Code besser geschrieben. Richtig?

Wenn es einem egal ist, ob der Patch auf die gesamte Testsitzung angewendet wird , ist der einfachste Weg, dies zu tun, ganz oben in der Testdatei:

# 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

Stellen Sie sicher, dass Sie die Datei mit dem Dekorator und nicht mit dem lokalen Bereich des Prüflings patchen und den Patch starten, bevor Sie das Gerät mit dem Dekorator importieren.

Interessanterweise wird auf alle bereits importierten Dateien der Patch auf den Dekorator angewendet, selbst wenn der Patch gestoppt wird. Dies ist die Umkehrung der Situation, mit der wir begonnen haben. Beachten Sie, dass diese Methode alle anderen Dateien im Testlauf patcht, die anschließend importiert werden - auch wenn sie selbst keinen Patch deklarieren.

user2859458
quelle
1
user2859458, das hat mir sehr geholfen. Die akzeptierte Antwort ist gut, aber dies hat die Dinge für mich auf sinnvolle Weise dargelegt und mehrere Anwendungsfälle eingeschlossen, in denen Sie möglicherweise etwas anderes wünschen.
Malcolm Jones
1
Vielen Dank für diese Antwort! Für den Fall, dass dies für andere nützlich ist, habe ich eine Patch-Erweiterung erstellt, die weiterhin als Kontextmanager fungiert und das Neuladen für Sie übernimmt
Geekfish
13

Als ich zum ersten Mal auf dieses Problem gestoßen bin, habe ich mir stundenlang den Kopf zerbrochen. Ich habe einen viel einfacheren Weg gefunden, damit umzugehen.

Dadurch wird der Dekorateur vollständig umgangen, da das Ziel überhaupt nicht dekoriert wurde.

Dies ist in zwei Teile gegliedert. Ich schlage vor, den folgenden Artikel zu lesen.

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

Zwei Fallstricke, denen ich immer wieder begegnete:

1.) Verspotten Sie den Decorator vor dem Import Ihrer Funktion / Ihres Moduls.

Die Dekoratoren und Funktionen werden zum Zeitpunkt des Ladens des Moduls definiert. Wenn Sie vor dem Import nicht verspotten, wird der Schein ignoriert. Nach dem Laden müssen Sie ein seltsames mock.patch.object ausführen, das noch frustrierender wird.

2.) Stellen Sie sicher, dass Sie den richtigen Weg zum Dekorateur verspotten.

Denken Sie daran, dass der Patch des Dekorators, den Sie verspotten, davon abhängt, wie Ihr Modul den Dekorator lädt, und nicht davon, wie Ihr Test den Dekorator lädt. Aus diesem Grund empfehle ich, für den Import immer vollständige Pfade zu verwenden. Dies erleichtert das Testen erheblich.

Schritte:

1.) Die Mock-Funktion:

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.) Den Dekorateur verspotten:

2a.) Pfad nach innen mit.

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

2b.) Patch oben in der Datei oder in TestCase.setUp

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

Mit beiden Methoden können Sie Ihre Funktion jederzeit in den TestCase oder dessen Methode / Testfälle importieren.

from mymodule import myfunction

2.) Verwenden Sie eine separate Funktion als Nebeneffekt des mock.patch.

Jetzt können Sie mock_decorator für jeden Dekorator verwenden, den Sie verspotten möchten. Sie müssen jeden Dekorateur einzeln verspotten, achten Sie also auf diejenigen, die Sie vermissen.

user7815681
quelle
1
Der von Ihnen zitierte Blog-Beitrag hat mir geholfen, dies viel besser zu verstehen!
Ritratt
2

Folgendes hat bei mir funktioniert:

  1. Beseitigen Sie die Importanweisung, die das Testziel lädt.
  2. Patchen Sie den Dekorator beim Teststart wie oben beschrieben.
  3. Rufen Sie importlib.import_module () sofort nach dem Patchen auf, um das Testziel zu laden.
  4. Führen Sie die Tests normal aus.

Es funktionierte wie ein Zauber.

Eric Mintz
quelle
1

Wir haben versucht, einen Dekorateur zu verspotten, der manchmal einen anderen Parameter wie einen String erhält, und manchmal nicht, z.

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

OR

@myDecorator
def function()

Dank einer der obigen Antworten haben wir eine Mock-Funktion geschrieben und den Dekorateur mit dieser Mock-Funktion gepatcht:

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

Beachten Sie, dass dieses Beispiel für einen Dekorateur geeignet ist, der die dekorierte Funktion nicht ausführt, sondern nur einige Dinge vor dem eigentlichen Lauf ausführt. Falls der Dekorator auch die dekorierte Funktion ausführt und daher die Parameter der Funktion übertragen muss, muss die Funktion mock_decorator etwas anders sein.

Hoffe das wird anderen helfen ...

InbalZelig
quelle
0

Vielleicht können Sie einen anderen Dekorator auf die Definitionen all Ihrer Dekoratoren anwenden, der im Grunde eine Konfigurationsvariable überprüft, um festzustellen, ob der Testmodus verwendet werden soll.
Wenn ja, ersetzt es den Dekorateur, den es dekoriert, durch einen Dummy-Dekorateur, der nichts tut.
Andernfalls lässt es diesen Dekorateur durch.

Aditya Mukherji
quelle
0

Konzept

Dies mag etwas seltsam klingen, aber man kann sys.pathmit einer Kopie von sich selbst patchen und einen Import im Rahmen der Testfunktion durchführen. Der folgende Code zeigt das Konzept.

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

MODULEkann dann durch das Modul ersetzt werden, das Sie testen. (Dies funktioniert in Python 3.6 mit MODULEersetzt durchxml zum Beispiel)

OP

Für Ihren Fall, lassen Sie uns die Dekorateur Funktion besteht in dem Modul sagen prettyund die geschmückten Funktion besteht in present, dann würden Sie flicken pretty.decoratordie Mock Maschinen und Ersatz Verwendung MODULEmit present. So etwas wie das Folgende sollte funktionieren (ungetestet).

Klasse TestDecorator (unittest.TestCase): ...

  @patch(`pretty.decorator`, decorator)
  @patch(`sys.path`, sys.path.copy())
  def testFunction(self, decorator) :
   import present
   ...

Erläuterung

Dies funktioniert, indem sys.pathfür jede Testfunktion eine "Bereinigung" bereitgestellt wird , wobei eine Kopie des Stroms sys.pathdes Testmoduls verwendet wird. Diese Kopie wird erstellt, wenn das Modul zum ersten Mal analysiert wird, um sicherzustellen, sys.pathdass alle Tests konsistent sind .

Nuancen

Es gibt jedoch einige Implikationen. Wenn das Testframework mehrere Testmodule unter derselben Python-Sitzung ausführt, MODULEbricht jedes Testmodul, das global importiert wird , jedes Testmodul, das es lokal importiert. Dies zwingt dazu, den Import überall lokal durchzuführen. Wenn das Framework jedes Testmodul unter einer separaten Python-Sitzung ausführt, sollte dies funktionieren. Ebenso können Sie möglicherweise nicht MODULEglobal in ein Testmodul importieren, in das Sie importierenMODULE lokal .

Die lokalen Importe müssen für jede Testfunktion innerhalb einer Unterklasse von durchgeführt werden unittest.TestCase. Es ist möglicherweise möglich, dies auf die unittest.TestCaseUnterklasse anzuwenden, um einen bestimmten Import des Moduls für alle Testfunktionen innerhalb der Klasse direkt verfügbar zu machen.

Eingebaute Ins

Diejenigen , Messing mit builtinImporten findet Ersatz MODULEmit sys, osusw. fehl, da diese auf alread werden , sys.pathwenn Sie versuchen , es zu kopieren. Der Trick hier ist, Python mit deaktivierten eingebauten Importen aufzurufen. Ich denke, das python -X test.pywird es tun, aber ich vergesse das entsprechende Flag (siehe python --help). Diese können anschließend lokal mit import builtinsIIRC importiert werden .

Carel
quelle
0

Um einen Dekorator zu patchen, müssen Sie entweder das Modul importieren oder neu laden, das diesen Dekorator nach dem Patchen verwendet, ODER den Verweis des Moduls auf diesen Dekorator insgesamt neu definieren.

Dekorateure werden zum Zeitpunkt des Imports eines Moduls angewendet. Wenn Sie ein Modul importieren, das einen Dekorator verwendet, den Sie oben in Ihrer Datei patchen möchten, und versuchen, es später zu patchen, ohne es erneut zu laden, hat der Patch keine Auswirkung.

Hier ist ein Beispiel für den ersten erwähnten Weg, dies zu tun - das Neuladen eines Moduls nach dem Patchen eines verwendeten Dekorators:

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()...

Hilfreiche Referenzen:

Arthur S.
quelle
-2

für @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

Wenn Sie einen Dekorator verwenden, der keine Parameter hat, sollten Sie:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated

Guochunyang
quelle
1
Ich sehe viele Probleme in dieser Antwort. Die erste (und die größere) ist, dass Sie keinen Zugriff auf die ursprüngliche Funktion haben können, wenn sie noch dekoriert ist (das ist das OP-Problem). Außerdem entfernen Sie den Patch nach Abschluss des Tests nicht. Dies kann zu Problemen führen, wenn Sie ihn in einer Testsuite ausführen.
Michele d'Amico