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


81

我有一个带有装饰器的函数,我正在Python Mock库的帮助下进行测试。我想mock.patch用一个仅调用函数的模拟“ bypass”装饰器代替真正的装饰器。

我不知道的是如何在真正的装饰器包装功能之前应用补丁。我在补丁目标上尝试了几种不同的变体,并对补丁和导入语句重新排序,但均未成功。有任何想法吗?

Answers:


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并装饰您的装饰器
IxDay 2014年

2
遇到@Paragon报告的问题,并通过在测试目录的中修补我的装饰器来解决此问题__init__。这样可以确保在任何测试文件之前都已加载补丁。我们有一个隔离的测试文件夹,因此该策略适用于我们,但这可能不适用于所有文件夹布局。
claytond

4
看了几次之后,我仍然感到困惑。这需要一个代码示例!
ritratt

@claytond感谢您的解决方案对我的帮助,因为我有一个隔离的测试文件夹!
Srivathsa

55

应当注意,这里的几个答案将为整个测试会话而不是单个测试实例打补丁装饰器。这可能是不可取的。这是修补仅在单个测试中存在的装饰器的方法。

我们的单元将与不需要的装饰器一起测试:

# 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的本地范围对文件进行修补,并启动修补程序。

有趣的是,即使补丁已停止,所有已导入的文件仍会将补丁应用于装饰器,这与我们开始时的情况相反。请注意,此方法将修补测试运行中随后导入的所有其他文件-即使它们自己未声明修补程序。


1
user2859458,这对我帮助很大。接受的答案是好的,但这对我来说是有意义的,并且包括多个用例,在这些用例中您可能需要一些不同的东西。
马尔科姆·琼斯

1
感谢您的回复!万一这对其他人有用,我做了一个扩展补丁,它仍然可以用作上下文管理器并为您重新加载:gist.github.com/Geekfish/aa43368ceade131b8ed9c822d2163373
Geekfish

12

当我第一次遇到这个问题时,我常常需要花费数小时来绞尽脑汁。我发现了一种更简单的方法来处理此问题。

这将完全绕过装饰器,就像目标甚至没有被装饰一样。

这分为两部分。我建议阅读以下文章。

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

我一直遇到的两个陷阱:

1.)在导入功能/模块之前模拟装饰器。

装饰器和函数是在模块加载时定义的。如果您在导入前不进行模拟,它将忽略该模拟。加载后,您必须执行一个怪异的mock.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()

这两种方式均允许您随时在TestCase或其方法/测试用例中导入函数。

from mymodule import myfunction

2)使用一个单独的函数作为模仿的补丁。

现在,您可以对要模拟的每个装饰器使用嘲笑装饰器。您将不得不分别模拟每个装饰器,因此请注意您错过的装饰器。


1
您引用的博客文章帮助我更好地理解了这一点!
ritratt

2

以下为我工作:

  1. 消除加载测试目标的import语句。
  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

请注意,此示例对于不运行装饰功能的装饰器很有用,它只在实际运行之前做一些事情。如果装饰器也运行装饰后的函数,因此需要传递该函数的参数,则模拟_装饰器函数必须有所不同。

希望这会帮助其他人...


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例如,为)

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进口会发现更换MODULEsysos等会失败,因为这些是在alreadsys.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问题)。此外,在完成测试后,您不会删除该修补程序,这可能会在测试套件中运行它时导致问题。
米歇尔·达米科
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.