关于单元测试:我可以在包装函数之前修补Python装饰器吗?

Can I patch a Python decorator before it wraps a function?

我在一个装饰器中有一个函数,我正试图在Python模拟库的帮助下进行测试。我想使用mock.patch用一个只调用函数的模拟"旁路"装饰器来替换真正的装饰器。我无法理解的是如何在真正的装饰器包装函数之前应用补丁。我在补丁目标上尝试了一些不同的变体,并重新排序了补丁和导入语句,但是没有成功。有什么想法吗?


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

所以,如果你想对一个装饰师进行修补,你需要做的是:

  • 导入包含它的模块
  • 定义模拟修饰函数
  • 设置,如module.decorator = mymockdecorator
  • 导入使用修饰器的模块,或在您自己的模块中使用它
  • 如果包含decorator的模块也包含使用它的函数,那么这些函数在您看到它们时就已经被修饰了,您可能是S.O.L。

    编辑以反映对python的更改,因为我最初写的是:如果装饰器使用functools.wraps()并且python的版本足够新,您可以使用__wrapped__属性挖掘原始函数并重新装饰它,但这并不能保证,而且您要替换的装饰器也可能不是唯一的装饰器。应用程序。


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

    我们要用不想要的装饰师测试的单元:

    1
    2
    3
    4
    5
    6
    7
    8
    # app/uut.py

    from app.decorators import func_decor

    @func_decor
    def unit_to_be_tested():
        # Do stuff
        pass

    来自Decorators模块:

    1
    2
    3
    4
    5
    6
    7
    # 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

    当我们的测试在测试运行期间被收集时,不需要的修饰器已经被应用到我们被测试的单元(因为这发生在导入时)。为了解决这个问题,我们需要手动替换decorator模块中的decorator,然后重新导入包含UUT的模块。

    我们的测试模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #  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

    清理回调、杀掉补丁、恢复原始装饰器并将其重新应用到我们测试的单元。这样,我们的补丁只通过一个测试而不是整个会话来保持——这正是其他补丁应该如何工作的。另外,由于clean up调用patch.stopall(),我们可以在需要的setup()中启动任何其他补丁,它们将在一个地方全部清除。

    理解这个方法的重要一点是,重新加载将如何影响事物。如果一个模块花费的时间太长,或者在导入时有运行的逻辑,那么您可能只需要耸耸肩并测试作为单元一部分的装饰器。:(希望您的代码写得更好。对吗?

    如果不关心补丁是否应用于整个测试会话,最简单的方法就是在测试文件的顶部:

    1
    2
    3
    4
    5
    6
    # 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

    确保使用decorator而不是uut的本地作用域修补文件,并在使用decorator导入单元之前启动修补程序。

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


    当我第一次遇到这个问题时,我常常绞尽脑汁几个小时。我找到了一个更容易处理的方法。

    这将完全绕过装饰,就像目标一开始甚至没有装饰。

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

    http://alexmarandon.com/articles/python_mock_gotchashan/

    我一直遇到的两个问题:

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

    在加载模块时定义装饰器和函数。如果你在进口前不模仿,它会无视模仿。加载之后,您必须执行一个奇怪的mock.patch.object,这会让人更加沮丧。

    2.)确保您正在模拟到装饰器的正确路径。

    记住,您模拟的decorator的补丁是基于模块如何加载decorator,而不是测试如何加载decorator。这就是为什么我建议总是使用完整的导入路径。这使得测试更加容易。

    步骤:

    1.)模拟功能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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.)内部通道。

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

    2b.)文件顶部或testcase.setup中的补丁

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

    这些方法中的任何一种都允许您在测试用例或其方法/测试用例中随时导入函数。

    1
    from mymodule import myfunction

    2.)使用单独的函数作为mock.patch的副作用。

    现在,您可以为每个想要模拟的装饰器使用模拟装饰器。你得分别模仿每个装饰师,所以要当心那些你错过的。


    以下内容对我很有用:

  • 消除加载测试目标的import语句。
  • 如上所述,在测试启动时修补decorator。
  • 修补后立即调用importlib.import_module()以加载测试目标。
  • 正常运行测试。
  • 这真是一种魅力。


    概念

    这听起来可能有点奇怪,但可以使用自身的副本对sys.path进行修补,并在测试功能范围内执行导入。下面的代码显示了这个概念。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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中有效,例如用xml替换MODULE)。

    操作

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

    类TestDecorator(UnitTest.TestCase):…

    1
    2
    3
    4
    5
      @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进口的人会发现,用sys取代MODULEos等将失败,因为当你试图复制它时,这些都在sys.path上读取。这里的技巧是在禁用内置导入的情况下调用python,我认为python -X test.py会这样做,但我忘记了适当的标志(参见python --help)。随后可使用import builtins和iirc在本地进口。


    也许您可以将另一个decorator应用到所有decorator的定义上,这些decorator基本上检查一些配置变量,以查看是否要使用测试模式。如果是,它将用一个不起任何作用的虚拟装饰器替换它正在装饰的装饰器。否则,它会让这个装饰器通过。


    对于@lru_缓存(最大_大小=1000)

    1
    2
    3
    4
    5
    6
    7
    8
    <wyn>class MockedLruCache(object):
    </p>

    [cc lang="python"]def __init__(self, maxsize=0, timeout=0):
        pass

    def __call__(self, func):
        return func

    cache.lrucache=模拟lrucache[/cc]

    如果使用没有参数的decorator,您应该:

    1
    2
    3
    4
    5
    6
    7
    <wyn>def MockAuthenticated(func):
        return func
    </p>

    <p>
    from tornado import web
    web.authenticated = MockAuthenticated

    代码>