我有一个带有装饰器的函数,我正在Python Mock库的帮助下进行测试。我想mock.patch
用一个仅调用函数的模拟“ bypass”装饰器代替真正的装饰器。
我不知道的是如何在真正的装饰器包装功能之前应用补丁。我在补丁目标上尝试了几种不同的变体,并对补丁和导入语句重新排序,但均未成功。有任何想法吗?
Answers:
装饰器在函数定义时应用。对于大多数功能,这是在模块加载时。(在其他函数中定义的函数会在每次调用封闭函数时应用装饰器。)
因此,如果您想用猴子修补装饰器,您需要做的是:
module.decorator = mymockdecorator
如果包含装饰器的模块也包含使用该装饰器的功能,那么在您看到它们时它们已经被装饰了,您可能就是SOL
自从我最初编写此代码以来,进行编辑以反映对Python的更改:如果装饰器使用functools.wraps()
并且Python版本足够新,则可以使用__wrapped__
属性来挖掘原始函数并重新装饰它,但这绝不是保证,并且您要替换的装饰器也可能不是唯一应用的装饰器。
reload
函数来重新生成python二进制代码docs.python.org/2/library/functions.html#reload并装饰您的装饰器
__init__
。这样可以确保在任何测试文件之前都已加载补丁。我们有一个隔离的测试文件夹,因此该策略适用于我们,但这可能不适用于所有文件夹布局。
应当注意,这里的几个答案将为整个测试会话而不是单个测试实例打补丁装饰器。这可能是不可取的。这是修补仅在单个测试中存在的装饰器的方法。
我们的单元将与不需要的装饰器一起测试:
# 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的本地范围对文件进行修补,并启动修补程序。
有趣的是,即使补丁已停止,所有已导入的文件仍会将补丁应用于装饰器,这与我们开始时的情况相反。请注意,此方法将修补测试运行中随后导入的所有其他文件-即使它们自己未声明修补程序。
当我第一次遇到这个问题时,我常常需要花费数小时来绞尽脑汁。我发现了一种更简单的方法来处理此问题。
这将完全绕过装饰器,就像目标甚至没有被装饰一样。
这分为两部分。我建议阅读以下文章。
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)使用一个单独的函数作为模仿的补丁。
现在,您可以对要模拟的每个装饰器使用嘲笑装饰器。您将不得不分别模拟每个装饰器,因此请注意您错过的装饰器。
以下为我工作:
它像魅力一样运作。
我们尝试模拟一个装饰器,该装饰器有时会获得另一个参数,例如字符串,有时却没有,例如:
@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
请注意,此示例对于不运行装饰功能的装饰器很有用,它只在实际运行之前做一些事情。如果装饰器也运行装饰后的函数,因此需要传递该函数的参数,则模拟_装饰器函数必须有所不同。
希望这会帮助其他人...
也许您可以将另一个装饰器应用于所有装饰器的定义,这些装饰器基本上检查一些配置变量以查看是否打算使用测试模式。
如果是,它将用不执行任何操作的虚拟装饰器替换正在装饰的装饰器。
否则,它将使此装饰器通过。
这听起来可能有些奇怪,但是可以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
例如,为)
对于你的情况,让我们说的装饰功能所在的模块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
进口会发现更换MODULE
用sys
,os
等会失败,因为这些是在alreadsys.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()...
有用的参考资料:
对于@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