Czy mogę załatać dekorator Pythona, zanim otoczy funkcję?

83

Mam funkcję z dekoratorem, którą próbuję przetestować za pomocą biblioteki Python Mock . Chciałbym użyćmock.patch do zastąpienia prawdziwego dekoratora fałszywym dekoratorem typu „bypass”, który po prostu wywołuje funkcję.

Nie potrafię pojąć, jak nałożyć łatkę, zanim prawdziwy dekorator zawinie funkcję. Wypróbowałem kilka różnych wariacji na temat celu poprawki i zmiany kolejności łat i instrukcji importu, ale bez powodzenia. Jakieś pomysły?

Chris Sears
źródło

Odpowiedzi:

59

Dekoratory są stosowane w czasie definiowania funkcji. W przypadku większości funkcji ma to miejsce podczas ładowania modułu. (Funkcje zdefiniowane w innych funkcjach mają dekorator stosowany za każdym razem, gdy wywoływana jest funkcja otaczająca).

Więc jeśli chcesz małpa łatać dekorator, musisz zrobić:

  1. Zaimportuj moduł, który go zawiera
  2. Zdefiniuj funkcję dekoratora pozorowanego
  3. Ustaw np module.decorator = mymockdecorator
  4. Zaimportuj moduły korzystające z dekoratora lub użyj go we własnym module

Jeśli moduł zawierający dekorator zawiera również funkcje, które go używają, te są już ozdobione, zanim je zobaczysz, i prawdopodobnie jesteś SOL

Edytuj, aby odzwierciedlić zmiany w Pythonie od czasu, gdy pierwotnie napisałem to: Jeśli dekorator używa, functools.wraps()a wersja Pythona jest wystarczająco nowa, możesz być w stanie wykopać oryginalną funkcję za pomocą __wrapped__atrybutu i ponownie ją ozdobić, ale w żadnym wypadku nie jest to jest gwarantowane, a dekorator, którego chcesz wymienić, może nie być jedynym zastosowanym dekoratorem.

kindall
źródło
17
Sporo czasu zmarnowałem: pamiętaj, że Python importuje moduły tylko raz. Jeśli uruchamiasz zestaw testów, próbujesz wyszydzić dekorator w jednym z testów, a funkcja dekorowania jest importowana gdzie indziej, kpienie z dekoratora nie przyniesie żadnego efektu.
Paragon
2
użyj wbudowanej reloadfunkcji, aby ponownie wygenerować kod binarny Pythona docs.python.org/2/library/functions.html#reload i monkeypatch your decorator
IxDay
3
Natknąłem się na problem zgłoszony przez @Paragon i obejść go, łatając mój dekorator w katalogu testowym __init__. To zapewniało, że łatka została załadowana przed jakimkolwiek plikiem testowym. Mamy izolowany folder testów, więc strategia działa dla nas, ale może to nie działać dla każdego układu folderów.
claytond
4
Po kilkukrotnym przeczytaniu tego nadal jestem zdezorientowany. To wymaga przykładu kodu!
ritratt
@claytond Dzięki, że Twoje rozwiązanie zadziałało, ponieważ miałem izolowany folder testów!
Srivathsa
56

Należy zauważyć, że kilka z przedstawionych tutaj odpowiedzi załatuje dekorator dla całej sesji testowej, a nie dla jednej instancji testowej; co może być niepożądane. Oto jak załatać dekorator, który utrzymuje się tylko przez jeden test.

Nasza jednostka do przetestowania z niepożądanym dekoratorem:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

Z modułu dekoratorów:

# 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

Zanim nasz test zostanie zebrany podczas przebiegu testowego, niepożądany dekorator został już zastosowany do naszej testowanej jednostki (ponieważ dzieje się to w czasie importu). Aby się tego pozbyć, musimy ręcznie wymienić dekorator w module dekoratora, a następnie ponownie zaimportować moduł zawierający nasz testowany egzemplarz.

Nasz moduł testowy:

#  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

Wywołanie zwrotne czyszczenia, kill_patches, przywraca oryginalny dekorator i ponownie stosuje go do testowanej jednostki. W ten sposób nasza łatka utrzymuje się tylko przez jeden test, a nie przez całą sesję - i dokładnie tak powinna zachowywać się każda inna łatka. Ponadto, ponieważ czyszczenie wywołuje patch.stopall (), możemy uruchomić dowolne inne poprawki w setUp (), których potrzebujemy, i zostaną one wyczyszczone w jednym miejscu.

Ważne jest, aby zrozumieć tę metodę, jak przeładowanie wpłynie na rzeczy. Jeśli moduł trwa zbyt długo lub ma logikę działającą podczas importu, wystarczy wzruszyć ramionami i przetestować dekorator jako część jednostki. :( Mam nadzieję, że twój kod jest lepiej napisany.

Jeśli nie obchodzi nas, czy łatka zostanie nałożona na całą sesję testową , najłatwiej to zrobić bezpośrednio na górze pliku testowego:

# 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

Pamiętaj, aby załatać plik za pomocą dekoratora, a nie lokalnego zakresu testowanego egzemplarza, i uruchomić poprawkę przed zaimportowaniem jednostki za pomocą dekoratora.

Co ciekawe, nawet jeśli łatka zostanie zatrzymana, wszystkie pliki, które zostały już zaimportowane, nadal będą miały poprawkę nałożoną na dekorator, co jest odwrotnością sytuacji, od której zaczęliśmy. Należy pamiętać, że ta metoda załatuje wszystkie inne pliki w przebiegu testowym, które są później importowane - nawet jeśli same nie deklarują poprawki.

user2859458
źródło
1
user2859458, to mi znacznie pomogło. Zaakceptowana odpowiedź jest dobra, ale opisała to dla mnie w znaczący sposób i obejmowała wiele przypadków użycia, w których możesz chcieć czegoś nieco innego.
Malcolm Jones,
1
Dziękuję za tę odpowiedź! Na wypadek, gdyby było to przydatne dla innych, stworzyłem rozszerzenie łatki, które nadal będzie działać jako menedżer kontekstu i przeładuje za Ciebie: gist.github.com/Geekfish/aa43368ceade131b8ed9c822d2163373
Geekfish
13

Kiedy pierwszy raz natknąłem się na ten problem, przez wiele godzin męczyłem mózg. Znalazłem znacznie łatwiejszy sposób, aby sobie z tym poradzić.

To całkowicie ominie dekoratora, tak jakby cel nie był w ogóle udekorowany.

Dzieli się to na dwie części. Proponuję przeczytać następujący artykuł.

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

Dwie pułapki, na które wpadałem:

1.) Mock the Decorator przed zaimportowaniem funkcji / modułu.

Dekoratory i funkcje są definiowane w momencie ładowania modułu. Jeśli nie kpisz przed zaimportowaniem, zignoruje to próbę. Po załadowaniu musisz wykonać dziwny mock.patch.object, co jest jeszcze bardziej frustrujące.

2.) Upewnij się, że kpisz z właściwej ścieżki do dekoratora.

Pamiętaj, że poprawka dekoratora, z którego kpisz, zależy od tego, jak moduł ładuje dekorator, a nie od tego, jak test ładuje dekorator. Dlatego radzę zawsze używać pełnych ścieżek do importu. To znacznie ułatwia testowanie.

Kroki:

1.) Funkcja 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.) Kpiny z dekoratora:

2a.) Ścieżka wewnątrz z.

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

2b.) Poprawka na górze pliku lub w TestCase.setUp

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

Każdy z tych sposobów pozwoli ci zaimportować twoją funkcję w dowolnym momencie w TestCase lub jej metodach / przypadkach testowych.

from mymodule import myfunction

2.) Użyj oddzielnej funkcji jako efektu ubocznego makiety.

Teraz możesz użyć mock_decorator dla każdego dekoratora, z którego chcesz kpić. Będziesz musiał kpić z każdego dekoratora osobno, więc uważaj na tych, których tęsknisz.

user7815681
źródło
1
Cytowany przez ciebie post na blogu pomógł mi to lepiej zrozumieć!
ritratt
2

Pracowały dla mnie:

  1. Usuń instrukcję importu, która ładuje cel testowy.
  2. Popraw dekorator podczas uruchamiania testu, jak opisano powyżej.
  3. Wywołaj importlib.import_module () natychmiast po zainstalowaniu poprawki, aby załadować cel testowy.
  4. Przeprowadź testy normalnie.

Zadziałało jak urok.

Eric Mintz
źródło
1

Próbowaliśmy kpić z dekoratora, który czasami pobiera inny parametr, jak string, a czasami nie, np .:

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

OR

@myDecorator
def function()

Dzięki jednej z powyższych odpowiedzi napisaliśmy funkcję próbną i załataliśmy dekorator z tą funkcją makiety:

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

Zauważ, że ten przykład jest dobry dla dekoratora, który nie uruchamia funkcji dekorowania, a tylko wykonuje pewne czynności przed właściwym uruchomieniem. W przypadku, gdy dekorator uruchamia również funkcję dekorowania, a zatem musi przekazać parametry funkcji, funkcja mock_decorator musi być nieco inna.

Mam nadzieję, że to pomoże innym ...

InbalZelig
źródło
0

Może możesz zastosować inny dekorator do definicji wszystkich swoich dekoratorów, który zasadniczo sprawdza niektóre zmienne konfiguracyjne, aby zobaczyć, czy ma być użyty tryb testowania.
Jeśli tak, zastępuje dekoratora, który ozdabia, sztucznym dekoratorem, który nic nie robi.
W przeciwnym razie przepuszcza ten dekorator.

Aditya Mukherji
źródło
0

Pojęcie

Może to zabrzmieć trochę dziwnie, ale można załatać sys.pathkopię samego siebie i wykonać import w zakresie funkcji testowej. Poniższy kod przedstawia koncepcję.

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

MODULEmożna wtedy zastąpić testowanym modułem. (Działa to w Pythonie 3.6 z MODULEpodstawionymxml na przykład)

OP

W Twoim przypadku, powiedzmy, że mieszka funkcyjne dekorator w module prettyi urządzone mieszka funkcyjnych present, wtedy można załatać pretty.decoratorza pomocą makiety maszyn i namiastkę MODULEz present. Powinno działać coś podobnego do następującego (nie przetestowano).

class TestDecorator (unittest.TestCase): ...

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

Wyjaśnienie

Działa to poprzez zapewnienie "czystego" sys.pathdla każdej funkcji testowej przy użyciu kopii prądu sys.pathmodułu testowego. Ta kopia jest tworzona podczas pierwszej analizy modułu, zapewniając spójność sys.pathwszystkich testów.

Niuanse

Istnieje jednak kilka konsekwencji. Jeśli środowisko testowe uruchamia wiele modułów testowych w tej samej sesji Pythona, każdy moduł testowy, który importuje MODULEglobalnie, uszkadza każdy moduł testowy, który importuje go lokalnie. To zmusza do wykonywania importu lokalnie wszędzie. Jeśli framework uruchamia każdy moduł testowy w oddzielnej sesji Pythona, to powinno działać. Podobnie nie możesz importować MODULEglobalnie w ramach modułu testowego, gdy importujesz MODULElokalnie.

Lokalne importy należy wykonać dla każdej funkcji testowej w podklasie unittest.TestCase. Być może można to zastosować unittest.TestCasebezpośrednio do podklasy, udostępniając określony import modułu dla wszystkich funkcji testowych w klasie.

Wbudowane Ins

Ci, brudząc z builtinimportu znajdzie wymianie MODULEz sys, ositd. Nie powiedzie się, ponieważ są one alread na sys.pathkiedy próbujesz go skopiować. Sztuczka polega na wywołaniu Pythona z wyłączonymi wbudowanymi importami, myślę, że python -X test.pyzrobię to, ale zapomnę o odpowiedniej fladze (zobacz python --help). Można je następnie importować lokalnie przy użyciu import builtinsIIRC.

Carel
źródło
0

Aby załatać dekorator, musisz zaimportować lub przeładować moduł, który używa tego dekoratora po jego łataniu, LUB całkowicie przedefiniować odniesienie modułu do tego dekoratora.

Dekoratory są stosowane podczas importowania modułu. Dlatego jeśli zaimportowałeś moduł, który używa dekoratora, który chcesz załatać na początku pliku, i spróbujesz go załatać później bez ponownego ładowania, łatka nie odniesie skutku.

Oto przykład pierwszego wspomnianego sposobu robienia tego - przeładowania modułu po łataniu dekoratora, którego używa:

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

Przydatne referencje:

Arthur S.
źródło
-2

dla @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

jeśli używasz dekoratora, który nie ma parametrów, powinieneś:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated

guochunyang
źródło
1
W tej odpowiedzi widzę wiele problemów. Pierwszą (i większą) jest to, że nie możesz mieć dostępu do oryginalnej funkcji, jeśli jest jeszcze udekorowana (to jest kwestia OP). Ponadto nie usuwasz łatki po zakończeniu testu, co może powodować problemy, gdy uruchomisz ją w zestawie testowym.
Michele d'Amico