Чи можу я виправити декоратор Python, перш ніж він оберне функцію?


83

У мене є функція з декоратором, яку я намагаюся протестувати за допомогою бібліотеки Python Mock . Я хотів би використати, mock.patchщоб замінити справжній декоратор на фіктивний декоратор обходу, який просто викликає функцію.

Я не можу зрозуміти, як застосувати патч до того, як справжній декоратор оберне функцію. Я спробував кілька різних варіантів цілі виправлення та змінив порядок виправлень виправлення та імпорту, але безуспішно. Будь-які ідеї?

Відповіді:


59

Декоратори застосовуються під час визначення функції. Для більшості функцій це коли модуль завантажується. (До функцій, визначених в інших функціях, декоратор застосовується кожного разу, коли викликається функція, що включає.)

Отже, якщо ви хочете, щоб мавпа залатала декоратора, вам потрібно:

  1. Імпортуйте модуль, який його містить
  2. Визначте функцію макетного декоратора
  3. Встановити, наприклад module.decorator = mymockdecorator
  4. Імпортуйте модулі, які використовують декоратор, або використовуйте їх у своєму власному модулі

Якщо модуль, який містить декоратор, також містить функції, які його використовують, вони вже оформлені до того часу, коли ви їх побачите, і ви, мабуть, SOL

Редагуйте, щоб відобразити зміни в Python, оскільки я спочатку писав це: якщо декоратор використовує functools.wraps()версію Python і є досить новою, можливо, ви зможете викопати оригінальну функцію за допомогою __wrapped__атрибута та повторно її прикрасити, але це аж ніяк не гарантовано, і декоратор, який ви хочете замінити, також може бути не єдиним застосовуваним декоратором.


17
Наступне змарнувало досить багато мого часу: майте на увазі, що Python імпортує модулі лише один раз. Якщо ви запускаєте набір тестів, намагаючись знущатись над декоратором в одному з ваших тестів, а декоровану функцію імпортуєте в інше місце, знущання над декоратором не дадуть ефекту.
Парагон

2
використовувати вбудовану reloadфункцію для регенерації двійкового коду python docs.python.org/2/library/functions.html#reload та monkeypatch вашого декоратора
IxDay

3
Натрапив на проблему, про яку повідомляє @Paragon, і обійшов її, виправивши мій декоратор у тестовому каталозі __init__. Це забезпечило завантаження виправлення перед будь-яким тестовим файлом. У нас є ізольована папка тестів, тому стратегія працює для нас, але це може працювати не для кожного макета папок.
claytond

4
Прочитавши це кілька разів, я все ще розгублений. Для цього потрібен приклад коду!
ritratt

@claytond Дякую, що ваше рішення працювало на мене, оскільки я мав ізольовану папку тестів!
Шріватса 03.03.20

56

Слід зазначити, що кілька відповідей тут будуть виправляти декоратор протягом усього тестового сеансу, а не окремий екземпляр тесту; що може бути небажаним. Ось як виправити декоратор, який зберігається лише під час одного тесту.

Наш пристрій, який буде протестовано з небажаним декоратором:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

З модуля декораторів:

# 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

На той час, коли наш тест збирається під час пробного запуску, небажаний декоратор вже був застосований до нашого випробовуваного блоку (оскільки це відбувається під час імпорту). Щоб позбутися цього, нам доведеться вручну замінити декоратор в модулі декоратора, а потім повторно імпортувати модуль, що містить наш UUT.

Наш тестовий модуль:

#  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

Зворотний виклик очищення, kill_patches, відновлює оригінальний декоратор і повторно застосовує його до блоку, який ми тестували. Таким чином, наш патч зберігається лише під час одного тесту, а не всього сеансу - саме так повинен поводитися будь-який інший патч. Крім того, оскільки очищення викликає patch.stopall (), ми можемо запустити будь-які інші потрібні нам патчі, і вони будуть очищені в одному місці.

Головне, що слід зрозуміти при цьому методі, - це те, як перезавантаження вплине на речі. Якщо модуль займає занадто багато часу або логіка працює під час імпорту, можливо, вам просто доведеться знизати плечима та протестувати декоратор як частину блоку. :( Сподіваємось, ваш код написаний краще, ніж це. Так?

Якщо вам байдуже, застосовується патч до всього тестового сеансу , найпростіший спосіб це зробити - у верхній частині тестового файлу:

# 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

Переконайтесь, що виправляєте файл за допомогою декоратора, а не локальної області UUT, і запускаєте виправлення перед імпортом блоку за допомогою декоратора.

Цікаво, що навіть якщо патч зупинено, усі файли, які вже імпортували, все одно матимуть патч до декоратора, що є зворотною ситуацією, з якої ми почали. Майте на увазі, що цей метод буде виправляти будь-які інші файли в тестовому запуску, які імпортуються згодом - навіть якщо вони не оголошують виправлення самостійно.


1
user2859458, це суттєво мені допомогло. Прийнята відповідь - це добре, але це висловило для мене зміст змістовно і включало кілька випадків використання, коли вам може знадобитися щось дещо інше.
Малкольм Джонс

1
Дякую за відповідь! На випадок, якщо це корисно для інших, я зробив розширення виправлення, яке все одно буде працювати як менеджер контексту, і зробить для вас перезавантаження: gist.github.com/Geekfish/aa43368ceade131b8ed9c822d2163373
Geekfish

13

Коли я вперше зіткнувся з цією проблемою, я годинами ламаю мозок. Я знайшов набагато простіший спосіб з цим впоратися.

Це повністю обійде декоратора, оскільки ціль навіть не була декорована спочатку.

Це розбито на дві частини. Пропоную прочитати наступну статтю.

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

Дві проблеми, з якими я постійно стикався:

1.) Знущайтеся над декоратором перед імпортом вашої функції / модуля.

Декоратори та функції визначаються під час завантаження модуля. Якщо ви не знущаєтесь перед імпортом, це не враховуватиме знущання. Після завантаження вам потрібно зробити дивний mock.patch.object, що ще більше засмучує.

2.) Переконайтеся, що ви глузуєте з правильного шляху до декоратора.

Пам'ятайте, що патч декоратора, з якого ви знущаєтесь, базується на тому, як ваш модуль завантажує декоратор, а не на те, як ваш тест завантажує декоратор. Ось чому я пропоную завжди використовувати повні шляхи для імпорту. Це значно полегшує тестування.

Кроки:

1.) Функція 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.) Знущання над декоратором:

2а.) Шлях всередині з.

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

2b.) Виправлення у верхній частині файлу або в TestCase.setUp

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

Будь-який із цих способів дозволить вам імпортувати свою функцію в будь-який час із TestCase або його методів / тестових випадків.

from mymodule import myfunction

2.) Використовуйте окрему функцію як побічний ефект mock.patch.

Тепер ви можете використовувати mock_decorator для кожного декоратора, з якого ви хочете знущатися. Вам доведеться знущатися над кожним декоратором окремо, тому стежте за тими, за якими ви сумуєте.


1
Повідомлення в блозі, яке ви цитували, допомогло мені зрозуміти це набагато краще!
ritratt

2

У мене працювало:

  1. Усуньте оператор імпорту, який завантажує тестову ціль.
  2. Виправити декоратор під час тестового запуску, як застосовано вище.
  3. Викличте importlib.import_module () відразу після виправлення, щоб завантажити тестову ціль.
  4. Запускайте тести нормально.

Це спрацювало як шарм.


1

Ми намагалися знущатись над декоратором, який іноді отримує інший параметр, такий як рядок, а іноді ні, наприклад:

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

OR

@myDecorator
def function()

Завдяки одній із відповідей вище, ми написали макетну функцію та підключили декоратор до цієї макетної функції:

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

Зверніть увагу, що цей приклад хороший для декоратора, який не запускає функцію оформлення, лише виконайте деякі речі до фактичного запуску. Якщо декоратор також запускає функцію декору, і, отже, йому потрібно передати параметри функції, функція mock_decorator повинна бути дещо іншою.

Сподіваюся, це допоможе іншим ...


0

Можливо, ви можете застосувати інший декоратор до визначень усіх ваших декораторів, який в основному перевіряє певну змінну конфігурації, щоб побачити, чи призначений режим тестування.
Якщо так, він замінює декоратора, який він прикрашає, фіктивним декоратором, який нічого не робить.
В іншому випадку він пропускає цей декоратор.


0

Концепція

Це може звучати трохи дивно, але можна виправити sys.path, скопіювавши себе, та виконати імпорт у межах тестової функції. Наступний код показує концепцію.

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тоді можна замінити модулем, який ви тестуєте. (Це працює в Python 3.6 з MODULEзаміненим наxml наприклад)

ОП

У вашому випадку, припустимо, функція декоратора знаходиться в модулі, prettyа функція декору - в ньому present, тоді ви виправляєте pretty.decoratorза допомогою макетного механізму і замінюєте MODULEна present. Щось на зразок наступного має працювати (Неперевірене).

клас TestDecorator (unittest.TestCase): ...

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

Пояснення

Це працює, забезпечуючи "чисту" sys.pathдля кожної тестової функції, використовуючи копію поточного sys.pathтестового модуля. Ця копія робиться при першому аналізі модуля, що забезпечує узгодженість sys.pathусіх тестів.

Нюанси

Однак є кілька наслідків. Якщо фреймворк тестування запускає кілька тестових модулів під одним і тим же сеансом python, будь-який тестовий модуль, який імпортує MODULEглобально, порушує будь-який тестовий модуль, який імпортує його локально. Це змушує виконувати імпорт локально скрізь. Якщо фреймворк запускає кожен тестовий модуль в рамках окремого сеансу python, це повинно працювати. Так само ви не можете імпортувати MODULEглобально в тестовому модулі, куди ви імпортуєте MODULEлокально.

Місцевий імпорт повинен виконуватися для кожної тестової функції в межах підкласу unittest.TestCase. Можливо, можливо застосувати це до unittest.TestCaseпідкласу безпосередньо, роблячи певний імпорт модуля доступним для всіх тестових функцій у класі.

Вбудований Ins

Ті , балуватися з builtinімпортом буде знайти заміну MODULEз sys, і osтак далі не вийде , так як вони alread на sys.pathпри спробі скопіювати його. Фокус тут полягає у тому, щоб викликати Python з відключеним вбудованим імпортом, я думаю python -X test.pyце зробить, але я забув відповідний прапор (Див. python --help). Згодом їх можна імпортувати локально за допомогою import builtinsIIRC.


0

Щоб виправити декоратор, вам потрібно або імпортувати, або перезавантажити модуль, який використовує цей декоратор, після його виправлення, АБО перевизначити посилання модуля на цей декоратор взагалі.

Декоратори застосовуються під час імпорту модуля. Ось чому, якщо ви імпортували модуль, який використовує декоратор, який ви хочете виправити у верхній частині вашого файлу, і спробувати його виправити пізніше, не перезавантажуючи, патч не матиме ефекту.

Ось приклад першого згаданого способу цього - перезавантаження модуля після виправлення декоратора, який він використовує:

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

Корисні посилання:


-2

для @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

якщо використовується декоратор, який не має параметрів, вам слід:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated


1
У цій відповіді я бачу багато питань. Перший (і більший) полягає в тому, що ви не можете отримати доступ до оригінальної функції, якщо вона ще оформлена (це проблема OP). Більше того, ви не видаляєте виправлення після закінчення тесту, і це може спричинити проблеми, коли ви запускаєте його в наборі тестів.
Мікеле д'Аміко,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.