我可以在 Python 修饰器包装函数之前修补它吗?

我有一个带装饰器的函数,我正试图在 Python Mock库的帮助下测试它。我想使用 mock.patch来替换真正的装饰器,使用一个模拟的“旁路”装饰器,它只调用函数。

我不知道的是如何在真正的装饰器包装函数之前应用补丁。我在补丁目标和重新排序补丁和导入语句上尝试了一些不同的变体,但没有成功。有什么想法吗?

65135 次浏览

修饰符在函数定义时应用。对于大多数函数来说,这是加载模块的时候。(在其他函数中定义的函数在每次调用封闭函数时都应用修饰符。)

因此,如果你想给一个装潢师贴补丁,你需要做的是:

  1. Import the module that contains it
  2. 定义模拟修饰器函数
  3. 设定 例如:。 module.decorator = mymockdecorator
  4. 导入使用装饰符的模块,或者在您自己的模块中使用它

如果包含装饰符的模块也包含使用它的函数,那么当您看到它们时,这些函数已经装饰好了,您很可能是 S.O.L。

编辑以反映对 Python 的修改,因为我最初写过这样的文章: 如果装饰器使用 functools.wraps(),并且 Python 的版本足够新,那么您可以使用 __wrapped__属性挖出原来的函数并重新装饰它,但是这并不能保证,并且您想要替换的装饰器也可能不是应用的唯一装饰器。

也许您可以在所有装饰器的定义上应用另一个装饰器,该装饰器基本上检查一些配置变量,以查看是否应该使用测试模式。
如果是,它将用一个什么都不做的假装的装饰器替换它正在装饰的装饰器。
否则,它会让这个装潢师通过。

以下几点对我很有效:

  1. 消除加载测试目标的导入语句。
  2. 在测试启动时修补上面应用的装饰器。
  3. 在修补之后立即调用 import lib.import _ module ()来加载测试目标。
  4. 正常运行测试。

效果非常好。

For@lru _ cache (max _ size = 1000)


class MockedLruCache(object):

def __init__(self, maxsize=0, timeout=0):
pass


def __call__(self, func):
return func

LruCache = MockedLruCache

如果使用没有参数的装饰器,你应该:

Def MockAuthenticated (func) :
返回函数

来自龙卷风进口网站 Authenticated = MockAuthenticated

应该注意的是,这里的几个答案将修补整个测试会话的装饰器,而不是一个单一的测试实例; 这可能是不可取的。下面介绍如何修补只在单个测试中持久化的装饰器。

我们的单位将被测试与不受欢迎的装饰:

# 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 _ patch 恢复原来的装饰器并将其重新应用到我们正在测试的单元。这样,我们的补丁只能通过一个测试而不是整个会话来持久存在——这正是任何其他补丁的行为方式。另外,由于清理调用 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 的本地作用域来修补文件,并且在使用修饰符导入单元之前启动修补程序。

有趣的是,即使补丁被停止,所有已经导入的文件仍然会将补丁应用到装饰器,这与我们开始时的情况相反。请注意,此方法将修补测试运行中随后导入的任何其他文件——即使它们本身没有声明修补程序。

When I first ran across this problem, I use to rack my brain for hours. I found a much easier way to handle this.

这样就能绕过装饰者,就好像目标根本没有装饰过一样。

本文分为两个部分。我建议阅读下面的文章。

Http://alexmarandon.com/articles/python_mock_gotchas/

我一直遇到的两个陷阱:

1)在导入你的函数/模块之前嘲笑装饰工具。

修饰符和函数在加载模块时定义。 如果在导入之前不调试,它将忽略调试。在加载之后,您必须执行一个奇怪的嘲笑.patch.object,这会让您更加沮丧。

2)确保你正在模仿装饰者的正确路径。

请记住,您正在模仿的装饰器的补丁是基于您的模块如何加载装饰器,而不是您的测试如何加载装饰器。这就是为什么我建议总是使用完整路径进行导入。这使得测试变得更加容易。

步骤:

1)模拟功能:

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

Either of these ways will allow you to import your function at anytime within the TestCase or its method/test cases.

from mymodule import myfunction

2)使用一个单独的函数作为嘲讽补丁的副作用。

现在,您可以对想要模仿的每个装饰器使用 mock _ decorator。你将不得不分别嘲笑每一个装修师,所以要小心那些你错过的。

概念

这听起来可能有点奇怪,但是我们可以使用自身的副本来修补 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 may then be substituted with the module you are testing. (This works in Python 3.6 with MODULE substituted with xml for example)

OP

对于您的情况,假设修饰函数驻留在模块 pretty中,修饰函数驻留在 present中,那么您将使用模拟机器修补 pretty.decorator,并用 present替换 MODULE。下面的内容应该可以(未经测试)。

类 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的任何测试模块都会中断本地导入 MODULE的任何测试模块。这迫使人们在任何地方都在本地执行导入。如果框架在一个单独的 python 会话下运行每个测试模块,那么这应该可以工作。类似地,您可能不会在本地导入 MODULE的测试模块中全局导入 MODULE

必须在 unittest.TestCase的子类中为每个测试函数执行本地导入。也许可以将此应用于 unittest.TestCase子类,直接使模块的特定导入可用于类中的所有测试函数。

嵌入式

那些扰乱 builtin导入的人会发现用 sysos等代替 MODULE将会失败,因为当你试图复制 sys.path时,它们已经被读取了。这里的诀窍是在禁用内置导入的情况下调用 Python,我认为 python -X test.py会这样做,但是我忘记了适当的标志(参见 python --help)。这些随后可以使用 import builtins,IIRC 在本地导入。

要修补装饰器,您需要导入或重新加载使用该装饰器 之后的模块,或者重新定义该模块对该装饰器的引用。

修饰符在导入模块时应用。这就是为什么如果您导入了一个模块,它使用了一个您想要在文件顶部进行修补的装饰器,并尝试在以后进行修补,而无需重新加载它,那么修补程序将不会产生任何效果。

下面是提到的第一种方法的例子——在修补了它使用的装饰器之后重新加载模块:

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

参考资料:

我们试图模仿一个装饰符,它有时会得到另一个参数,比如字符串,有时则不会,例如:

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


OR


@myDecorator
def function()

多亏了上面的一个答案,我们编写了一个 mock 函数,并用这个 mock 函数修补了装饰器:

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 _ decator 函数必须有所不同。

Hope this will help others...

我喜欢做一个更简单,更容易理解的把戏。利用装饰器的功能,创建一个旁路。

模拟函数:

from functools import wraps


def the_call(*args, **kwargs):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
if kwargs.pop("bypass", None) is True:
return function(*args, **kwargs)
# You will probably do something that will change the response or the arguments here below
args = ("bar")
kwargs = {"stuff": "bar"}
return function(*args, **kwargs)
return wrapper
return decorator

你和室内设计师的关系:

@the_call()
def my_simple_function(stuff: str):
return stuff




print(my_simple_function(stuff="Hello World"))

Will return:

“酒吧”

So in your tests, simply pass the parameter bypass = True

print(my_simple_function(stuff="Hello World", bypass=True))

将返回:

"Hello World"

那些搞乱内置导入的人会发现用 sys、 os 等代替 MODULE 将会失败,因为当您尝试复制它们时,这些已经在 sys.path 上了。这里的技巧是在禁用内置导入的情况下调用 Python,我认为 Python-X test.py 会这样做,但是我忘记了适当的标志(请参阅 Python —— help)。这些可以随后使用导入内置程序 IIRC 在本地导入。

我好像有点头绪了。重要的是,一个测试应该总是保持它发现的东西... ... 但这里只有一个其他的答案似乎解决了这一点: 如果你正在替换一个模仿或假的一个真正的装饰者,你必须在测试后恢复真正的装饰者。

在 thread _ check.py 模块中,我有一个名为 thread_check的修饰器,它(这是一个 PyQt5上下文)检查是否在“右线程”中调用了一个函数或方法(比如桂或非桂)。它看起来像这样:

def thread_check(gui_thread: bool):
def pseudo_decorator(func):
if not callable(func):
raise Exception(f'func is type {type(func)}')
def inner_function(*args, **kwargs):
if QtWidgets.QApplication.instance() != None:
app_thread = QtWidgets.QApplication.instance().thread()
curr_thread = QtCore.QThread.currentThread()
if gui_thread != None:
if (curr_thread == app_thread) != gui_thread:
raise Exception(f'method {func.__qualname__} should have been called in {"GUI thread" if gui_thread else "non-GUI thread"}')
return func(*args, **kwargs)
return inner_function
return pseudo_decorator

In practice, in my case here, it makes more sense in most cases to patch out this decorator completely, for all tests, with a "do-nothing decorator" at the start of each run. But to illustrate how it can be done on a per-test basis, see below.

所提出的问题是,类 AbstractLongRunningTaskis_thread_interrupt_req(实际上它不是抽象的: 您可以实例化它)这样的方法必须在非桂线程中运行。方法是这样的:

@thread_check(False) # i.e. non-Gui thread
def is_thread_interrupt_req(self):
return self.thread.isInterruptionRequested()

这就是我如何解决修补 thread_check装饰器的问题的方法,通过清理“模块空间”来为下一个测试恢复真正的装饰器:

@pytest.fixture
def restore_tm_classes():
yield
importlib.reload(task_manager_classes)


@pytest.mark.parametrize('is_ir_result', [True, False]) # return value from QThread.isInterruptionRequested()
@mock.patch('PyQt5.QtCore.QThread.isInterruptionRequested')
def test_ALRT_is_thread_interrupt_req_returns_val_of_thread_isInterruptionRequested(mock_is_ir, request, qtbot, is_ir_result, restore_tm_classes):
print(f'\n>>>>>> test: {request.node.nodeid}')
print(f'thread_check.thread_check {thread_check.thread_check}')


def do_nothing_decorator(gui_thread):
def pseudo_decorator(func):
return func
return pseudo_decorator
    

with mock.patch('thread_check.thread_check', side_effect=do_nothing_decorator):
importlib.reload(task_manager_classes)
with mock.patch('PyQt5.QtCore.QThread.start'): # NB the constructor calls QThread.start(): must be mocked!
tm = task_manager_classes.TaskManager(task_manager_classes.AbstractLongRunningTask)
mock_is_ir.return_value = is_ir_result
assert tm.task.is_thread_interrupt_req() == is_ir_result
    

    

def test_another(request):
print(f'\n>>>>>> test: {request.node.nodeid}')
print(f'thread_check.thread_check {thread_check.thread_check}')

test_another节目中,我们打印出以下内容:

thread_check.thread_check <function thread_check at 0x000002234BEABE50>

就是 test_ALRT...测试开始时打印出来的物体。

这里的关键是在你的补丁中使用 side_effectimportlib.reload组合来重新加载你的模块,这个模块本身将使用装饰器。

请注意这里的上下文管理器缩进: thread_check.thread_check上的补丁只需要应用到 reload... 在调用实际方法(is_thread_interrupt_req)时,假装的装饰器已经就位。

如果你 don't使用这个拆卸装置 restore_tm_classes,这里有一些很奇怪的事情发生: 事实上在下一个测试方法中,它看起来(根据我的实验) ,装饰器既不是真的,也不是 do_nothing_decorator,正如我通过在两者中都放入 print语句所确定的那样。因此,如果你不通过重新加载调整过的模块来恢复,那么在测试套件期间,task_manager_classes模块中的应用程序代码就会留下一个“僵尸装饰器”(它似乎什么也不做)。

注意
当您在测试运行的中间使用 importlib.reload时,存在很大的潜在问题。

特别是,它可以证明应用程序代码使用具有某个 id 值的类 X (即 id(MyClass)) ,但是测试代码(在这个模块中,随后运行模块)使用的应该是同一个类 X,但是具有另一个 id 值!有时这可能无关紧要,其他时候它可能导致一些相当令人困惑的失败测试,这可能是可以解决的,但可能需要您

  1. prefer to avoid mock.patching objects which have not been created actually inside the test: when for example a class itself (I'm not thinking here of an 类的对象, but 类作为变量本身) is imported or created outside any tests and thus is created 在测试收集阶段: in this case the class object will not be the same as the one after the reload.

  2. 甚至使用 importlib.reload(...)内的一些夹具在各种模块,以前工作没有这一点!

始终使用 pytest-random-order(多次运行)来揭示此类(和其他)问题的全部范围。

正如我所说,可以在运行开始时简单地修补装饰器。因此,是否值得这样做是另一回事。我实际上实现了相反的情况: 在运行开始时修补了 thread_check装饰器,然后使用上述 importlib技术修补了 patched back in,用于一两个需要装饰器操作的测试。