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

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

Ответы [ 7 ]

49 голосов
/ 06 октября 2011

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

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

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

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

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

35 голосов
/ 18 июня 2016

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

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

# 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

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

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

#  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

Убедитесь, что патч файла выполняется с помощью декоратора, а не локальной области проверяемого оборудования, и запустите патч перед импортом устройства с декоратором.

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

4 голосов
/ 04 апреля 2017

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

Это полностью обойдет декоратор, как если бы цель даже не была оформлена с самого начала.

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

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

Два Gotchas, с которыми я продолжал сталкиваться:

1.) Насмехайтесь над Decorator перед импортом вашей функции / модуля.

Декораторы и функции определяются во время загрузки модуля.Если вы не издеваетесь перед импортом, он игнорирует макет.После загрузки вы должны сделать странный объект 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.) Насмешка над декоратором:

2a.) Путь внутрь с помощью.

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 голос
/ 17 января 2014

Для меня сработало следующее:

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

Это сработало как чудо.

0 голосов
/ 13 мая 2018

Концепция

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

Объяснение

Это работает путем предоставления «чистого» * ​​1026 * для каждой тестовой функции с использованием копии текущего 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.

0 голосов
/ 20 ноября 2015

для @lru_cache (max_size = 1000)

<br>
<code>class MockedLruCache(object):</p>

<pre><code>def __init__(self, maxsize=0, timeout=0):
    pass

def __call__(self, func):
    return func
</code>

cache.LruCache = MockedLruCache

если вы используете декоратор без параметров, вы должны:

<code>def MockAuthenticated(func):
    return func</p>

<p>from tornado import web
web.authenticated = MockAuthenticated 
0 голосов
/ 06 октября 2011

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

...