我可以在 Python 装饰器包装函数之前对其进行修补吗?
我有一个带有装饰器的函数,我正在尝试借助 Python Mock 图书馆。我想使用 mock.patch
将真正的装饰器替换为仅调用该函数的模拟“旁路”装饰器。
我不明白的是如何在真正的装饰器包装函数之前应用补丁。我已经尝试了补丁目标的几种不同变体,并重新排序了补丁和导入语句,但没有成功。有什么想法吗?
I have a function with a decorator that I'm trying test with the help of the Python Mock library. I'd like to use mock.patch
to replace the real decorator with a mock 'bypass' decorator which just calls the function.
What I can't figure out is how to apply the patch before the real decorator wraps the function. I've tried a few different variations on the patch target and reordering the patch and import statements, but without success. Any ideas?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(15)
应该注意的是,这里的几个答案将为整个测试会话而不是单个测试实例修补装饰器;这可能是不可取的。以下是如何修补仅通过一次测试持续存在的装饰器。
我们的单元将使用不需要的装饰器进行测试:
来自装饰器模块:
在测试运行期间收集我们的测试时,不需要的装饰器已经应用于我们的被测单元(因为这发生在导入时)。为了摆脱这个问题,我们需要手动替换装饰器模块中的装饰器,然后重新导入包含 UUT 的模块。
我们的测试模块:
清理回调,kill_patches,恢复原始装饰器并将其重新应用到我们正在测试的单元。这样,我们的补丁仅在一次测试中持续存在,而不是在整个会话中持续存在——这正是任何其他补丁应有的行为。另外,由于清理调用 patch.stopall(),我们可以在 setUp() 中启动我们需要的任何其他补丁,它们将在一处全部清理干净。
关于此方法,需要了解的重要一点是重新加载将如何影响事物。如果模块花费的时间太长或具有在导入时运行的逻辑,您可能只需要耸耸肩并将装饰器作为单元的一部分进行测试。 :( 希望你的代码写得比这更好。对吧?
如果不关心补丁是否应用于整个测试会话,最简单的方法就是在顶部测试文件:
确保使用装饰器而不是 UUT 的本地范围修补文件,并在使用装饰器导入单元之前启动修补程序,
有趣的是,即使修补程序停止,所有已导入的文件仍然会。将补丁应用于装饰器,即与我们开始的情况相反,请注意,此方法将修补测试运行中随后导入的任何其他文件 - 即使它们本身没有声明补丁。
It should be noted that several of the answers here will patch the decorator for the entire test session rather than a single test instance; which may be undesirable. Here's how to patch a decorator that only persists through a single test.
Our unit to be tested with the undesired decorator:
From decorators module:
By the time our test gets collected during a test run, the undesired decorator has already been applied to our unit under test (because that happens at import time). In order to get rid of that, we'll need to manually replace the decorator in the decorator's module and then re-import the module containing our UUT.
Our test module:
The cleanup callback, kill_patches, restores the original decorator and re-applies it to the unit we were testing. This way, our patch only persists through a single test rather than the entire session -- which is exactly how any other patch should behave. Also, since the clean up calls patch.stopall(), we can start any other patches in the setUp() we need and they will get cleaned up all in one place.
The important thing to understand about this method is how the reloading will affect things. If a module takes too long or has logic that runs on import, you may just need to shrug and test the decorator as part of the unit. :( Hopefully your code is better written than that. Right?
If one doesn't care if the patch is applied to the whole test session, the easiest way to do that is right at the top of the test file:
Make sure to patch the file with the decorator rather than the local scope of the UUT and to start the patch before importing the unit with the decorator.
Interestingly, even if the patch is stopped, all the files that already imported will still have the patch applied to the decorator, which is the reverse of the situation we started with. Be aware that this method will patch any other files in the test run that are imported afterwards -- even if they don't declare a patch themselves.
装饰器在函数定义时应用。对于大多数功能来说,这是加载模块的时间。 (在其他函数中定义的函数每次调用封闭函数时都会应用装饰器。)
因此,如果您想对装饰器进行猴子修补,您需要做的是:
module.decorator = mymockdecorator
如果包含装饰器的模块还包含以下函数使用它,当你看到它们时,它们已经被装饰了,并且你可能是 SOL
编辑来反映 Python 的更改,因为我最初写了这个:如果装饰器使用
functools.wraps()
并且Python 版本足够新,你也许可以使用 __wrapped__ 属性挖出原来的函数并重新装饰它,但这绝不是保证,而且你想要替换的装饰器也是如此可能不是唯一的应用了装饰器。Decorators are applied at function definition time. For most functions, this is when the module is loaded. (Functions that are defined in other functions have the decorator applied each time the enclosing function is called.)
So if you want to monkey-patch a decorator, what you need to do is:
module.decorator = mymockdecorator
If the module that contains the decorator also contains functions that use it, those are already decorated by the time you can see them, and you're probably S.O.L.
Edit to reflect changes to Python since I originally wrote this: If the decorator uses
functools.wraps()
and the version of Python is new enough, you may be able to dig out the original function using the__wrapped__
attribute and re-decorate it, but this is by no means guaranteed, and the decorator you want to replace also may not be the only decorator applied.当我第一次遇到这个问题时,我常常绞尽脑汁好几个小时。我找到了一种更简单的方法来处理这个问题。
这将完全绕过装饰器,就像目标一开始就没有被装饰一样。
这分为两部分。我建议阅读下面的文章。
1
我一直遇到的两个问题:
。 )在导入函数/模块之前模拟装饰器。
装饰器和函数是在加载模块时定义的。
如果您在导入之前不进行模拟,它将忽略模拟。加载后,您必须执行一个奇怪的mock.patch.object,这会变得更加令人沮丧。
2.) 确保您正在模拟装饰器的正确路径。
请记住,您正在模拟的装饰器的补丁基于您的模块如何加载装饰器,而不是您的测试如何加载装饰器。这就是为什么我建议始终使用完整路径进行导入。这使得测试变得更加容易。
步骤:
1.) Mock 函数:
2.) 模拟装饰器:
2a.) 内部路径。
2b.) 在文件顶部或在 TestCase.setUp 中打补丁
这两种方式都允许您随时在 TestCase 或其方法/测试用例中导入函数。
2.) 使用单独的函数作为mock.patch 的副作用。
现在您可以对每个要模拟的装饰器使用mock_decorator。您必须分别模拟每个装饰器,因此请注意您错过的装饰器。
When I first ran across this problem, I use to rack my brain for hours. I found a much easier way to handle this.
This will fully bypass the decorator, like the target wasn't even decorated in the first place.
This is broken down into two parts. I suggest reading the following article.
http://alexmarandon.com/articles/python_mock_gotchas/
Two Gotchas that I kept running into:
1.) Mock the Decorator before the import of your function/module.
The decorators and functions are defined at the time the module is loaded.
If you do not mock before import, it will disregard the mock. After load, you have to do a weird mock.patch.object, which gets even more frustrating.
2.) Make sure you are mocking the correct path to the decorator.
Remember that the patch of the decorator you are mocking is based on how your module loads the decorator, not how your test loads the decorator. This is why I suggest always using full paths for imports. This makes things a lot easier for testing.
Steps:
1.) The Mock function:
2.) Mocking the decorator:
2a.) Path inside with.
2b.) Patch at top of file, or in TestCase.setUp
Either of these ways will allow you to import your function at anytime within the TestCase or its method/test cases.
2.) Use a separate function as a side effect of the mock.patch.
Now you can use mock_decorator for each decorator you want to mock. You will have to mock each decorator separately, so watch out for the ones you miss.
我们尝试模拟一个装饰器,它有时会获取另一个参数,例如字符串,有时则不会,例如:
由于上面的答案之一,我们编写了一个模拟函数并使用此模拟函数修补装饰器:
请注意,此示例是对于不运行装饰函数的装饰器来说很有用,只在实际运行之前做一些事情。
如果装饰器也运行被装饰的函数,因此它需要传输函数的参数,则mock_decorator函数必须有点不同。
希望这会帮助其他人...
We tried to mock a decorator that sometimes gets another parameter like a string, and some times not, eg.:
Thanks to one of the answers above, we wrote a mock function and patch the decorator with this mock function:
Note that this example is good for a decorator that doesn't run the decorated function, only do some stuff before the actual run.
In case the decorator also runs the decorated function, and hence it needs to transfer the function's parameters, the mock_decorator function has to be a bit different.
Hope this will help others...
以下对我有用:
它就像一个魅力。
The following worked for me:
It worked like a charm.
要修补装饰器,您需要在修补后导入或重新加载使用该装饰器的模块,或者完全重新定义模块对该装饰器的引用。
导入模块的时间。这就是为什么如果您导入了一个使用要在文件顶部修补的装饰器的模块,并尝试稍后对其进行修补而不重新加载它,则该修补程序将无效。
以下是提到的第一种方法的示例 - 在修补模块使用的装饰器后重新加载模块:
有用的参考:
imp.reload
的 Python 3 文档重新加载
的Python 2.7文档To patch a decorator, you need to either import or reload the module which uses that decorator after patching it OR redefine the module's reference to that decorator altogether.
Decorators are applied at the time that a module is imported. This is why if you imported a module which uses a decorator you want to patch at the top of your file and attempt it to patch it later on without reloading it, the patch would have no effect.
Here is an example of the first way mentioned of doing this - reloading a module after patching a decorator it uses:
Helpful References:
imp.reload
reload
概念
这可能听起来有点奇怪,但我们可以使用其自身的副本来修补 sys.path,并在测试函数的范围内执行导入。下面的代码展示了这个概念。
然后,
MODULE
可以替换为您正在测试的模块。 (这适用于Python 3.6,例如用MODULE
替换为xml
)OP
对于您的情况,假设装饰器函数驻留在模块
pretty
中并且装饰函数驻留在present
中,然后您可以使用模拟机制修补pretty.decorator
并用present
替换MODULE
代码>.像下面这样的东西应该可以工作(未经测试)。类 TestDecorator(unittest.TestCase) :
...
说明
这是通过使用测试模块当前
sys.path
的副本为每个测试函数提供“干净的”sys.path
来实现的。该副本是在首次解析模块时创建的,确保所有测试的 sys.path 一致。然而
,存在一些细微差别。如果测试框架在同一个 python 会话下运行多个测试模块,则全局导入 MODULE 的任何测试模块都会破坏本地导入它的任何测试模块。这迫使人们在任何地方进行本地导入。如果框架在单独的 python 会话下运行每个测试模块,那么这应该可以工作。同样,您不能在本地导入
MODULE
的测试模块中全局导入MODULE
。必须对
unittest.TestCase
子类中的每个测试函数进行本地导入。也许可以将其直接应用于unittest.TestCase
子类,从而使模块的特定导入可用于该类中的所有测试函数。内置
那些搞乱
builtin
导入的人会发现用sys
、os
等替换MODULE
将会失败,因为这些当您尝试复制它时,它们已经在sys.path
上。这里的技巧是在禁用内置导入的情况下调用 Python,我认为 python -X test.py 可以做到这一点,但我忘记了适当的标志(请参阅 python --help )。随后可以使用importbuiltins
、IIRC 在本地导入这些内容。Concept
This may sound a bit odd but one can patch
sys.path
, with a copy of itself, and perform an import within the scope of the test function. The following code shows the concept.MODULE
may then be substituted with the module you are testing. (This works in Python 3.6 withMODULE
substituted withxml
for example)OP
For your case, let's say the decorator function resides in the module
pretty
and the decorated function resides inpresent
, then you would patchpretty.decorator
using the mock machinery and substituteMODULE
withpresent
. Something like the following should work (Untested).class TestDecorator(unittest.TestCase) :
...
Explanation
This works by providing a "clean"
sys.path
for each test function, using a copy of the currentsys.path
of the test module. This copy is made when the module is first parsed ensuring a consistentsys.path
for all the tests.Nuances
There are a few implications, however. If the testing framework runs multiple test modules under the same python session any test module that imports
MODULE
globally breaks any test module that imports it locally. This forces one to perform the import locally everywhere. If the framework runs each test module under a separate python session then this should work. Similarly you may not importMODULE
globally within a test module where you're importingMODULE
locally.The local imports must be done for each test function within a subclass of
unittest.TestCase
. It is perhaps possible to apply this to theunittest.TestCase
subclass directly making a particular import of the module available for all of the test functions within the class.Built Ins
Those messing with
builtin
imports will find replacingMODULE
withsys
,os
etc. will fail, since these are alread onsys.path
when you try to copy it. The trick here is to invoke Python with the builtin imports disabled, I thinkpython -X test.py
will do it but I forget the appropriate flag (Seepython --help
). These may subsequently be imported locally usingimport builtins
, IIRC.我似乎对此有所了解。重要的是,测试应该始终保留发现的东西......但这里只有一个其他答案似乎解决了这一点:如果你用模拟或假的来代替真正的装饰器,你必须恢复那个真正的装饰器测试后。
在模块 thread_check.py 中,我有一个名为
thread_check
的装饰器,它(这是 PyQt5 上下文)检查是否在“正确的线程”(即 Gui 或非 Gui)中调用了函数或方法。它看起来像这样:在实践中,就我而言,在大多数情况下,对于所有测试,在每次运行开始时使用“不执行任何操作的装饰器”来完全修补此装饰器更有意义。但为了说明如何在每次测试的基础上完成它,请参见下文。
提出的问题是,诸如
AbstractLongRunningTask
类的is_thread_interrupt_req
之类的方法(实际上它不是抽象的:您可以实例化它)必须在非 Gui 线程中运行。所以方法看起来像这样:这就是我如何解决修补
thread_check
装饰器的问题,以清理“模块空间”以恢复真正的装饰器以进行下一个测试的方式:..在
test_another
中,我们得到以下打印结果:... 这与
test_ALRT...
测试开始时打印的对象相同。这里的关键是在补丁中使用
side_effect
与importlib.reload
结合使用来重新加载本身将使用装饰器的模块。请注意此处的上下文管理器缩进:
thread_check.thread_check
上的补丁只需在实际方法 (is_thread_interrupt_req
) 时应用于reload
... code>) 被调用,假装饰器就位。如果您不使用这个拆卸装置
restore_tm_classes
,这里会发生一些非常奇怪的事情:事实上,在下一个测试方法中,它会出现(根据我的实验)装饰器既不是真正的装饰器,也不是do_nothing_decorator
,正如我通过在两者中放入print
语句所确定的那样。因此,如果您不通过重新加载调整后的模块来恢复,那么在测试套件的持续时间内,task_manager_classes
模块中的应用程序代码似乎会留下一个“僵尸装饰器”(显示为什么都不做)。警告
当您在测试运行过程中使用 importlib.reload 时,可能会出现很大的问题。
特别是,可以发现应用程序代码正在使用具有特定 id 值(即
id(MyClass)
)的类 X,但测试代码(在此模块和随后运行的模块中)使用的应该是相同的 X 类,但具有不同的 id 值!有时这可能并不重要,有时它可能会导致一些相当令人困惑的失败测试,这些测试可能可以解决,但可能需要您更喜欢避免
mock.patch
ing对象。实际上是在测试中创建的:例如,当导入类本身时(我在这里不是考虑类的对象,而是类作为变量本身)或在任何测试之外创建,因此被创建在测试收集阶段:在这种情况下,类对象将与重新加载后的不同。甚至可以在以前没有此功能的各个模块中的一些固定装置中使用
importlib.reload(...)
!始终使用
pytest-random-order
(多次运行)揭示此类(和其他)问题的全部范围。正如我所说,装饰器可以简单地在运行开始时进行修补。因此是否值得这样做是另一回事。事实上,我实现了相反的情况:
thread_check
装饰器在运行开始时被修补,但随后修补回来,使用上述 importlib 技术,进行一两个需要装饰器运行的测试。I seem to have got somewhere with this. It's important that a test should always leave things as it found them... but only one of the other answers here seems to address that point: if you are substituting a mock or fake for a real decorator, you have to restore that real decorator after the test.
In module thread_check.py I have a decorator called
thread_check
which (this is a PyQt5 context) checks to see that a function or method is called in the "right thread" (i.e. Gui or non-Gui). It looks like this: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.
The problem posed is that a method such as
is_thread_interrupt_req
of classAbstractLongRunningTask
(in fact it's not abstract: you can instantiate it) must be run in a non-Gui thread. So the method looks like this:This is how I solved the question of patching the
thread_check
decorator, in a way which cleans up the "module space" to restore the real decorator for the next test:... in
test_another
we get the following printed out:... which is the same object as was printed out at the start of the
test_ALRT...
test.The key here is to use
side_effect
in your patch in combination withimportlib.reload
to reload your module which is itself going to use the decorator.Note the context manager indenting here: the patch on
thread_check.thread_check
only needs to apply to thereload
... by the time the actual method (is_thread_interrupt_req
) is called, the fake decorator is in place.There is something quite strange going on here if you don't use this teardown fixture
restore_tm_classes
: in fact in the next test method, it then appears (from my experiments) that the decorator will neither be the real one nor thedo_nothing_decorator
, as I ascertained by putting inprint
statements in both. So if you don't restore by reloading the tweaked module it appears that the app code in thetask_manager_classes
module is then left, for the duration of the test suite, with a "zombie decorator" (which appears to do nothing).Caveat
There are big potential problems when you use
importlib.reload
in the middle of a test run.In particular it can then turn out that the app code is using class X with a certain id value (i.e.
id(MyClass)
) but the test code (in this and subsequently run modules) is using supposedly the same class X but having another id value! Sometimes this may not matter, other times it can lead to some rather baffling failed tests, which can probably be solved, but may require you toprefer to avoid
mock.patch
ing objects which have not been created actually inside the test: when for example a class itself (I'm not thinking here of an object of a class, but the class as variable itself) is imported or created outside any tests and thus is created in the test collection phase: in this case the class object will not be the same as the one after the reload.even to use
importlib.reload(...)
inside some fixtures in various modules which had previously worked without this!Always use
pytest-random-order
(with multiple runs) to reveal the full extent of such (and other) problems.As I said, the decorator could simply be patched out at the start of the run. Whether it's therefore worth doing this is another matter. I have in fact implemented the reverse situation: where the
thread_check
decorator is patched out at the start of the run, but then patched back in, using the aboveimportlib
techniques, for one or two tests which need the decorator to be operative.定义模拟装饰器
在导入模块之前应用补丁
3.正确构建测试
4.运行测试
Define the Mock Decorator
Apply the patch before importing the module
3.Structure your tests properly
4.Run your tests
也许您可以将另一个装饰器应用到所有装饰器的定义上,该装饰器基本上检查一些配置变量以查看是否要使用测试模式。
如果是,它将用不执行任何操作的虚拟装饰器替换它正在装饰的装饰器。
否则,它会让这个装饰器通过。
Maybe you can apply another decorator onto the definitions of all your decorators that basically checks some configuration variable to see if testing mode is meant to be used.
If yes, it replaces the decorator it is decorating with a dummy decorator that does nothing.
Otherwise, it lets this decorator through.
那些搞乱内置导入的人会发现用 sys、os 等替换 MODULE 会失败,因为当您尝试复制它时,这些已经在 sys.path 上了。这里的技巧是在禁用内置导入的情况下调用 Python,我认为 python -X test.py 可以做到这一点,但我忘记了适当的标志(请参阅 python --help)。随后可以使用导入内置命令 IIRC 在本地导入这些内容。
Those messing with builtin imports will find replacing MODULE with sys, os etc. will fail, since these are alread on sys.path when you try to copy it. The trick here is to invoke Python with the builtin imports disabled, I think python -X test.py will do it but I forget the appropriate flag (See python --help). These may subsequently be imported locally using import builtins, IIRC.
简而言之:
请注意,它可能会删除所有装饰器,因此请随意检查其名称:)
In short:
Note that it might remove all decorators so feel free to check for its name :)
对我有用的方法基本上是取消装饰它。
我在
myapp.module_a
中有这个函数在这里尝试了其他方法(对我来说它们有意义),但他们没有使该函数不重试测试。
对我有用的是将以下代码放在
tests/__init__.py
中,如果你想为所有测试修补它,在你想要禁用它之前The way that worked for me was basically to undecorate it.
I have this function in
myapp.module_a
Tried other methods here (that for me they have sense), but they did not make the function to not retry on the tests.
What worked for me was to put the following code in
tests/__init__.py
if you want to patch it for all tests, of before where you want to disable it我喜欢让技巧变得更简单、更容易理解。利用装饰器的功能并创建旁路。
模拟函数:
带有装饰器的函数:
将返回:
在你的测试中,只要传递参数bypass = True
就会返回:
I like to make a trick simpler and easier to understand. Take advantage of the decorator's functionality and create a bypass.
The mock function:
Your function with the decorator:
Will return:
So in your tests, simply pass the parameter bypass = True
Will return:
对于@lru_cache(max_size = 1000)
for @lru_cache(max_size=1000)