Подтвердить что ты не робот

Могу ли я исправить декоратор Python, прежде чем он обернет функцию?

У меня есть функция с декоратором, которую я пытаюсь проверить с помощью библиотеки Python Mock. Я хотел бы использовать mock.patch для замены реального декоратора макетным обходным декоратором, который просто вызывает функцию. Я не могу понять, как применить патч до того, как реальный декоратор обернет функцию. Я пробовал несколько разных вариантов целевого патча и переупорядочивал инструкции патча и импорта, но безуспешно. Любые идеи?

4b9b3361

Ответ 1

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

Так что, если вы хотите сделать обезьяны-патчи для декоратора, вам нужно сделать следующее:

  1. Импортировать содержащий его модуль
  2. Определить фиктивную функцию декоратора
  3. Установите, например, module.decorator = mymockdecorator
  4. Импортируйте модули, которые используют декоратор, или используйте его в своем собственном модуле

Если модуль, который содержит декоратор, также содержит функции, которые его используют, они уже оформлены к тому времени, когда вы их видите, и вы, вероятно, S.O.L.

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

Ответ 2

Следует отметить, что некоторые из ответов здесь будут исправлять декоратор для всего тестового сеанса, а не только одного тестового экземпляра; что может быть нежелательным. Здесь, как исправлять декоратор, который сохраняется только через один тест.

Наш прибор для тестирования с нежелательным декоратором:

# 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(), мы можем запускать любые другие исправления в setUp(), которые нам нужны, и они будут очищены в одном месте.

Важное значение для понимания этого метода - это то, как перезагрузка повлияет на вещи. Если модуль занимает слишком много времени или имеет логику, выполняемую при импорте, вам просто нужно пожать плечами и протестировать декоратор как часть устройства.:( Надеюсь, ваш код лучше написан, чем это. Правильно?

Если неважно, применяется ли патч для всего тестового сеанса, самый простой способ сделать это в верхней части тестового файла:

# 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, и запустите патч перед импортом устройства с помощью декоратора.

Интересно, что даже если патч остановлен, все файлы, которые уже были импортированы, по-прежнему будут иметь патч, примененный к декоратору, что является обратной ситуацией, с которой мы начали. Имейте в виду, что этот метод будет исправлять любые другие файлы в тестовом прогоне, которые впоследствии импортируются, даже если они сами не объявляют патч.

Ответ 3

Когда я впервые столкнулся с этой проблемой, я часами ломал голову. Я нашел гораздо более простой способ справиться с этим.

Это полностью обойдёт декоратор, как будто цель даже не была декорирована.

Это разбито на две части. Предлагаю прочитать следующую статью.

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

Два 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 для каждого декоратора, который хотите макетировать. Вам придется издеваться над каждым декоратором отдельно, так что следите за тем, кого вы пропустили.

Ответ 4

Следующие работали для меня:

  • Устранить оператор импорта, который загружает тестовую цель.
  • Замените декоратор при запуске теста, как описано выше.
  • Вызов importlib.import_module() сразу после исправления для загрузки тестовой цели.
  • Обычно запускать тесты.

Он работал как шарм.

Ответ 5

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

Ответ 6

Концепция

Это может звучать немного странно, но можно исправить 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)

OP

В вашем случае, допустим, функция декоратора находится в модуле 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, что сделает конкретный импорт модуля доступным для всех тестовых функций в классе.

Встроенные модули

Те, кто возится с импортом builtin, обнаружат, что замена MODULE на sys, os и т.д. Не удастся, так как они считываются в sys.path при попытке его скопировать. Хитрость здесь в том, чтобы вызвать Python с отключенным встроенным импортом, я думаю, что python -X test.py сделает это, но я забыл соответствующий флаг (см. python --help). Впоследствии они могут быть импортированы локально с использованием import builtins, IIRC.

Ответ 7

Чтобы установить исправление для декоратора, вам необходимо либо импортировать, либо перезагрузить модуль, который использует этот декоратор, после его исправления ИЛИ переопределить ссылку на модуль для этого декоратора.

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

Вот пример первого упомянутого способа сделать это - перезагрузить модуль после исправления используемого им декоратора:

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

Полезные ссылки:

Ответ 8

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