web-dev-qa-db-ger.com

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

Ich habe eine Funktion mit einem Dekorateur, den ich mit Hilfe der Python Mock library teste. Ich möchte mock.patch verwenden, um den echten Dekorateur durch einen scheinbaren "Bypass" -Dekorateur zu ersetzen, der nur die Funktion aufruft. Was ich nicht herausfinden kann, ist, wie der Patch angewendet wird, bevor der echte Dekorateur die Funktion umschließt. Ich habe ein paar verschiedene Varianten des Patch-Ziels ausprobiert und die Patch- und Import-Anweisungen neu geordnet, jedoch ohne Erfolg. Irgendwelche Ideen?

53
Chris Sears

Dekoratoren werden zum Zeitpunkt der Funktionsdefinition angewendet. Bei den meisten Funktionen ist dies der Fall, wenn das Modul geladen ist. (Bei Funktionen, die in anderen Funktionen definiert sind, wird der Dekorator bei jedem Aufruf der einschließenden Funktion angewendet.)

Wenn Sie also einen Dekorateur mit einem Affen-Patch versehen möchten, müssen Sie Folgendes tun:

  1. Importieren Sie das Modul, das es enthält
  2. Definieren Sie die Schein-Dekorationsfunktion
  3. Set 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 zu dem Zeitpunkt dekoriert, zu dem Sie sie sehen können, und Sie sind wahrscheinlich S.O.L. 

Bearbeiten, um Änderungen an Python widerzuspiegeln, seit ich das ursprünglich geschrieben habe: Wenn der Dekorateur functools.wraps() verwendet und die Version von Python neu genug ist, können Sie die ursprüngliche Funktion mit __wrapped__ attritube herausfinden und neu dekorieren, dies ist jedoch vorbei Es gibt keine Garantie, und der Dekorateur, den Sie ersetzen möchten, ist möglicherweise nicht der einzige Dekorator, der angewendet wird.

42
kindall

Es sollte beachtet werden, dass einige der Antworten hier den Decorator für die gesamte Testsitzung patchen anstatt einer einzelnen Testinstanz; was unerwünscht sein kann. So können Sie einen Dekorateur patchen, der nur durch einen einzigen Test bestehen bleibt.

Unser Gerät wird mit dem unerwünschten Dekorateur getestet:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

Vom Dekorateurmodul:

# 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 gesammelt wurde, wurde der unerwünschte Dekorateur bereits auf unser zu testendes Gerät angewendet (weil dies zum Zeitpunkt des Importierens geschieht). Um dies zu vermeiden, müssen wir den Dekorateur manuell im Modul des Dekorators austauschen und das Modul, das unser UUT enthält, 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 Cleanup-Rückruf kill_patches stellt den ursprünglichen Dekorator wieder her und wendet ihn erneut auf die getestete Einheit an. Auf diese Weise bleibt unser Patch nur für einen einzigen Test und nicht für die gesamte Sitzung bestehen. Genau so sollte sich jeder andere Patch verhalten. Da die Bereinigung patch.stopall () aufruft, können wir auch alle anderen benötigten Patches in setUp () starten, die an einem Ort aufgeräumt werden.

Das Wichtigste, was Sie über diese Methode wissen müssen, ist, wie sich das Nachladen auf die Dinge auswirkt. Wenn ein Modul zu lange benötigt oder eine Logik beim Import ausgeführt wird, müssen Sie den Dekorateur möglicherweise nur mit den Schultern zucken und als Teil der Einheit testen. :( Hoffentlich wird Ihr Code besser geschrieben als das. Richtig?

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

# 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 Dekorateur 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 der Patch auch beim Stoppen des Patches für alle bereits importierten Dateien auf den Dekorierer angewendet. Dies ist die Umkehrung der Situation, mit der wir begonnen haben. Beachten Sie, dass mit dieser Methode alle anderen Dateien im Testlauf gepatcht werden, die anschließend importiert werden - auch wenn sie selbst keinen Patch deklarieren.

31
user2859458

Als ich zum ersten Mal auf dieses Problem gestoßen bin, zerbreche ich mein Gehirn stundenlang. Ich habe einen viel einfacheren Weg gefunden, damit umzugehen.

Dadurch wird der Dekorateur vollständig umgangen, als wäre das Ziel überhaupt nicht dekoriert worden.

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

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

Zwei Gotchas, denen ich immer wieder begegnet bin:

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

Die Dekoratoren und Funktionen werden zu dem Zeitpunkt definiert, zu dem das Modul geladen wird .. Wenn Sie vor dem Importieren keine Modifikationen vornehmen, wird der Mock ignoriert. Nach dem Laden müssen Sie ein seltsames mock.patch.object ausführen, was noch frustrierender wird.

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

Denken Sie daran, dass der Patch des Dekorators, den Sie verspotten, davon abhängt, wie Ihr Modul den Dekorateur lädt, und nicht, wie der Test den Dekorateur lädt. Deshalb empfehle ich, immer vollständige Pfade für den Import zu verwenden. Das macht das Testen viel einfacher.

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 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 innerhalb des Testfalls oder seiner 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 separat verspotten, achten Sie also auf diejenigen, die Sie vermissen.

3
user7815681

Folgendes hat für mich gearbeitet:

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

Es hat wie ein Zauber funktioniert.

1
Eric Mintz

Möglicherweise können Sie einen anderen Dekorator auf die Definitionen aller Ihrer Dekorateure anwenden, der im Wesentlichen einige Konfigurationsvariablen überprüft, um zu sehen, ob der Testmodus verwendet werden soll.
Wenn ja, wird der Dekorateur durch einen Dummy-Dekorator ersetzt, der nichts tut.
Ansonsten lässt dieser Dekorateur durch.

0
Aditya Mukherji

Konzept

Das hört sich vielleicht etwas seltsam an, aber man kann sys.path mit einer Kopie von sich selbst patchen und im Rahmen der Testfunktion einen Import 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

MODULE kann dann durch das Modul ersetzt werden, das Sie testen. (Dies funktioniert in Python 3.6, wobei MODULE beispielsweise durch xml ersetzt wird.)

OP

Angenommen, die Decorator-Funktion befindet sich in Ihrem Modul pretty und die dekorierte Funktion in present. Dann würden Sie pretty.decorator mithilfe der Mock-Maschinerie patchen und MODULE durch present ersetzen. 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 für jede Testfunktion ein "sauberer" sys.path bereitgestellt wird, wobei eine Kopie des aktuellen sys.path des Testmoduls verwendet wird. Diese Kopie wird erstellt, wenn das Modul zum ersten Mal analysiert wird, um einen konsistenten sys.path für alle Tests sicherzustellen.

Nuancen

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

Die lokalen Importe müssen für jede Testfunktion innerhalb einer Unterklasse von unittest.TestCase durchgeführt werden. Es ist möglicherweise möglich, dies auf die Unterklasse unittest.TestCase anzuwenden, wodurch ein bestimmter Import des Moduls für alle Testfunktionen innerhalb der Klasse verfügbar wird.

Eingebaute Ins

Diejenigen, die mit builtin-Importen durcheinander geraten, werden feststellen, dass MODULE durch sys, os usw. ersetzt wird, da dies bereits auf sys.path erfolgt, wenn Sie versuchen, es zu kopieren. Der Trick hier ist, Python mit deaktivierten eingebauten Importen aufzurufen. Ich denke, python -X test.py wird es tun, aber ich vergesse das entsprechende Flag (Siehe python --help). Diese können anschließend lokal mit import builtins, IIRC importiert werden.

0
Carel