Can I patch a Python decorator before it wraps a function?
我在一个装饰器中有一个函数,我正试图在Python模拟库的帮助下进行测试。我想使用mock.patch用一个只调用函数的模拟"旁路"装饰器来替换真正的装饰器。我无法理解的是如何在真正的装饰器包装函数之前应用补丁。我在补丁目标上尝试了一些不同的变体,并重新排序了补丁和导入语句,但是没有成功。有什么想法吗?
修饰符在函数定义时应用。对于大多数功能,这是加载模块的时候。(在其他函数中定义的函数在每次调用封闭函数时都应用了修饰符。)
所以,如果你想对一个装饰师进行修补,你需要做的是:
如果包含decorator的模块也包含使用它的函数,那么这些函数在您看到它们时就已经被修饰了,您可能是S.O.L。
编辑以反映对python的更改,因为我最初写的是:如果装饰器使用
应该注意的是,这里的几个答案将修补整个测试会话的装饰器,而不是单个测试实例;这可能是不可取的。下面是如何修补一个只通过一个测试持续存在的装饰器。
我们要用不想要的装饰师测试的单元:
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的副作用。
现在,您可以为每个想要模拟的装饰器使用模拟装饰器。你得分别模仿每个装饰师,所以要当心那些你错过的。
以下内容对我很有用:
这真是一种魅力。
概念
这听起来可能有点奇怪,但可以使用自身的副本对
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 |
然后,可以用正在测试的模块替换
对于您的情况,假设decorator函数驻留在模块
类TestDecorator(UnitTest.TestCase):…
1 2 3 4 5 | @patch(`pretty.decorator`, decorator) @patch(`sys.path`, sys.path.copy()) def testFunction(self, decorator) : import present ... |
解释
这是通过为每个测试功能提供一个"干净"的
然而,这其中有一些含义。如果测试框架在同一个python会话下运行多个测试模块,那么任何导入
必须为
那些干扰
也许您可以将另一个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 |
代码>