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

发布于 2024-12-08 09:44:52 字数 261 浏览 0 评论 0原文

我有一个带有装饰器的函数,我正在尝试借助 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(15

披肩女神 2024-12-15 09:44:53

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

我们的单元将使用不需要的装饰器进行测试:

# 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
from importlib import reload # 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()
            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
        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 的本地范围修补文件,并在使用装饰器导入单元之前启动修补程序,

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

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:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

From decorators module:

# 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

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:

#  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
from importlib import reload # 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()
            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
        reload(uut)  # Reloads the uut.py module which applies our patched decorator

        

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:

# 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

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.

就像说晚安 2024-12-15 09:44:53

装饰器在函数定义时应用。对于大多数功能来说,这是加载模块的时间。 (在其他函数中定义的函数每次调用封闭函数时都会应用装饰器。)

因此,如果您想对装饰器进行猴子修补,您需要做的是:

  1. 导入包含它的模块
  2. 定义模拟装饰器函数
  3. 设置eg module.decorator = mymockdecorator
  4. 导入使用装饰器的模块,或者在您自己的模块中使用它

如果包含装饰器的模块还包含以下函数使用它,当你看到它们时,它们已经被装饰了,并且你可能是 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:

  1. Import the module that contains it
  2. Define the mock decorator function
  3. Set e.g. module.decorator = mymockdecorator
  4. Import the module(s) that use the decorator, or use it in your own module

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.

救星 2024-12-15 09:44:53

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

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

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

1

我一直遇到的两个问题:

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

装饰器和函数是在加载模块时定义的。
如果您在导入之前不进行模拟,它将忽略模拟。加载后,您必须执行一个奇怪的mock.patch.object,这会变得更加令人沮丧。

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

请记住,您正在模拟的装饰器的补丁基于您的模块如何加载装饰器,而不是您的测试如何加载装饰器。这就是为什么我建议始终使用完整路径进行导入。这使得测试变得更加容易。

步骤:

1.) Mock 函数:

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.) 使用单独的函数作为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:

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.) Mocking the decorator:

2a.) Path inside with.

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

2b.) Patch at top of file, or in TestCase.setUp

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

Either of these ways will allow you to import your function at anytime within the TestCase or its method/test cases.

from mymodule import myfunction

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.

无人接听 2024-12-15 09:44:53

我们尝试模拟一个装饰器,它有时会获取另一个参数,例如字符串,有时则不会,例如:

@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

请注意,此示例是对于不运行装饰函数的装饰器来说很有用,只在实际运行之前做一些事情。
如果装饰器也运行被装饰的函数,因此它需要传输函数的参数,则mock_decorator函数必须有点不同。

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

We tried to mock a decorator that sometimes gets another parameter like a string, and some times not, eg.:

@myDecorator('my-str')
def function()

OR

@myDecorator
def function()

Thanks to one of the answers above, we wrote a mock function and patch the decorator with this mock 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

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...

慈悲佛祖 2024-12-15 09:44:53

以下对我有用:

  1. 消除加载测试目标的导入语句。
  2. 如上面所应用的,在测试启动时修补装饰器。
  3. 修补后立即调用 importlib.import_module() 以加载测试目标。
  4. 正常运行测试。

它就像一个魅力。

The following worked for me:

  1. Eliminate the import statement that loads the test target.
  2. Patch the decorator on test startup as applied above.
  3. Invoke importlib.import_module() immediately after patching to load the test target.
  4. Run tests normally.

It worked like a charm.

弱骨蛰伏 2024-12-15 09:44:53

要修补装饰器,您需要在修补后导入或重新加载使用该装饰器的模块,或者完全重新定义模块对该装饰器的引用。

导入模块的时间。这就是为什么如果您导入了一个使用要在文件顶部修补的装饰器的模块,并尝试稍后对其进行修补而不重新加载它,则该修补程序将无效。

以下是提到的第一种方法的示例 - 在修补模块使用的装饰器后重新加载模块:

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()...

有用的参考:

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:

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()...

Helpful References:

铁憨憨 2024-12-15 09:44:53

概念

这可能听起来有点奇怪,但我们可以使用其自身的副本来修补 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 并用 present 替换 MODULE代码>.像下面这样的东西应该可以工作(未经测试)。

类 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导入的人会发现用sysos等替换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.

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 may then be substituted with the module you are testing. (This works in Python 3.6 with MODULE substituted with xml for example)

OP

For your case, let's say the decorator function resides in the module pretty and the decorated function resides in present, then you would patch pretty.decorator using the mock machinery and substitute MODULE with present. Something like the following should work (Untested).

class TestDecorator(unittest.TestCase) :
...

  @patch(`pretty.decorator`, decorator)
  @patch(`sys.path`, sys.path.copy())
  def testFunction(self, decorator) :
   import present
   ...

Explanation

This works by providing a "clean" sys.path for each test function, using a copy of the current sys.path of the test module. This copy is made when the module is first parsed ensuring a consistent sys.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 import MODULE globally within a test module where you're importing MODULE 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 the unittest.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 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.

方圜几里 2024-12-15 09:44:53

我似乎对此有所了解。重要的是,测试应该始终保留发现的东西......但这里只有一个其他答案似乎解决了这一点:如果你用模拟或假的来代替真正的装饰器,你必须恢复那个真正的装饰器测试后。

在模块 thread_check.py 中,我有一个名为 thread_check 的装饰器,它(这是 PyQt5 上下文)检查是否在“正确的线程”(即 Gui 或非 Gui)中调用了函数或方法。它看起来像这样:

def thread_check(gui_thread: bool):
    def pseudo_decorator(func):
        if not callable(func):
            raise Exception(f'func is type {type(func)}')
        def inner_function(*args, **kwargs):
            if QtWidgets.QApplication.instance() != None: 
                app_thread = QtWidgets.QApplication.instance().thread()
                curr_thread = QtCore.QThread.currentThread()
                if gui_thread != None:
                    if (curr_thread == app_thread) != gui_thread:
                        raise Exception(f'method {func.__qualname__} should have been called in {"GUI thread" if gui_thread else "non-GUI thread"}')
            return func(*args, **kwargs)
        return inner_function
    return pseudo_decorator

在实践中,就我而言,在大多数情况下,对于所有测试,在每次运行开始时使用“不执行任何操作的装饰器”来完全修补此装饰器更有意义。但为了说明如何在每次测试的基础上完成它,请参见下文。

提出的问题是,诸如 AbstractLongRunningTask 类的 is_thread_interrupt_req 之类的方法(实际上它不是抽象的:您可以实例化它)必须在非 Gui 线程中运行。所以方法看起来像这样:

@thread_check(False) # i.e. non-Gui thread
def is_thread_interrupt_req(self):
    return self.thread.isInterruptionRequested()

这就是我如何解决修补 thread_check 装饰器的问题,以清理“模块空间”以恢复真正的装饰器以进行下一个测试的方式:

@pytest.fixture    
def restore_tm_classes():
    yield
    importlib.reload(task_manager_classes)

@pytest.mark.parametrize('is_ir_result', [True, False]) # return value from QThread.isInterruptionRequested()
@mock.patch('PyQt5.QtCore.QThread.isInterruptionRequested')    
def test_ALRT_is_thread_interrupt_req_returns_val_of_thread_isInterruptionRequested(mock_is_ir, request, qtbot, is_ir_result, restore_tm_classes):
    print(f'\n>>>>>> test: {request.node.nodeid}')
    print(f'thread_check.thread_check {thread_check.thread_check}')

    def do_nothing_decorator(gui_thread):
        def pseudo_decorator(func):
            return func
        return pseudo_decorator
    
    with mock.patch('thread_check.thread_check', side_effect=do_nothing_decorator):
        importlib.reload(task_manager_classes)
    with mock.patch('PyQt5.QtCore.QThread.start'): # NB the constructor calls QThread.start(): must be mocked!
        tm = task_manager_classes.TaskManager(task_manager_classes.AbstractLongRunningTask)
    mock_is_ir.return_value = is_ir_result
    assert tm.task.is_thread_interrupt_req() == is_ir_result
    
    
def test_another(request): 
    print(f'\n>>>>>> test: {request.node.nodeid}')
    print(f'thread_check.thread_check {thread_check.thread_check}')

..在 test_another 中,我们得到以下打印结果:

thread_check.thread_check <function thread_check at 0x000002234BEABE50>

... 这与 test_ALRT... 测试开始时打印的对象相同。

这里的关键是在补丁中使用 side_effectimportlib.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 值!有时这可能并不重要,有时它可能会导致一些相当令人困惑的失败测试,​​这些测试可能可以解决,但可能需要您

  1. 更喜欢避免mock.patching对象。实际上是在测试中创建的:例如,当导入类本身时(我在这里不是考虑类的对象,而是类作为变量本身)或在任何测试之外创建,因此被创建在测试收集阶段:在这种情况下,类对象将与重新加载后的不同。

  2. 甚至可以在以前没有此功能的各个模块中的一些固定装置中使用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:

def thread_check(gui_thread: bool):
    def pseudo_decorator(func):
        if not callable(func):
            raise Exception(f'func is type {type(func)}')
        def inner_function(*args, **kwargs):
            if QtWidgets.QApplication.instance() != None: 
                app_thread = QtWidgets.QApplication.instance().thread()
                curr_thread = QtCore.QThread.currentThread()
                if gui_thread != None:
                    if (curr_thread == app_thread) != gui_thread:
                        raise Exception(f'method {func.__qualname__} should have been called in {"GUI thread" if gui_thread else "non-GUI thread"}')
            return func(*args, **kwargs)
        return inner_function
    return pseudo_decorator

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 class AbstractLongRunningTask (in fact it's not abstract: you can instantiate it) must be run in a non-Gui thread. So the method looks like this:

@thread_check(False) # i.e. non-Gui thread
def is_thread_interrupt_req(self):
    return self.thread.isInterruptionRequested()

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:

@pytest.fixture    
def restore_tm_classes():
    yield
    importlib.reload(task_manager_classes)

@pytest.mark.parametrize('is_ir_result', [True, False]) # return value from QThread.isInterruptionRequested()
@mock.patch('PyQt5.QtCore.QThread.isInterruptionRequested')    
def test_ALRT_is_thread_interrupt_req_returns_val_of_thread_isInterruptionRequested(mock_is_ir, request, qtbot, is_ir_result, restore_tm_classes):
    print(f'\n>>>>>> test: {request.node.nodeid}')
    print(f'thread_check.thread_check {thread_check.thread_check}')

    def do_nothing_decorator(gui_thread):
        def pseudo_decorator(func):
            return func
        return pseudo_decorator
    
    with mock.patch('thread_check.thread_check', side_effect=do_nothing_decorator):
        importlib.reload(task_manager_classes)
    with mock.patch('PyQt5.QtCore.QThread.start'): # NB the constructor calls QThread.start(): must be mocked!
        tm = task_manager_classes.TaskManager(task_manager_classes.AbstractLongRunningTask)
    mock_is_ir.return_value = is_ir_result
    assert tm.task.is_thread_interrupt_req() == is_ir_result
    
    
def test_another(request): 
    print(f'\n>>>>>> test: {request.node.nodeid}')
    print(f'thread_check.thread_check {thread_check.thread_check}')

... in test_another we get the following printed out:

thread_check.thread_check <function thread_check at 0x000002234BEABE50>

... 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 with importlib.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 the reload... 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 the do_nothing_decorator, as I ascertained by putting in print statements in both. So if you don't restore by reloading the tweaked module it appears that the app code in the task_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 to

  1. prefer to avoid mock.patching 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.

  2. 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 above importlib techniques, for one or two tests which need the decorator to be operative.

半岛未凉 2024-12-15 09:44:53
  1. 定义模拟装饰器

    defbypass_decorator(func):
         返回函数
    
  2. 在导入模块之前应用补丁

    导入单元测试
    从unittest.mock导入补丁
    与 patch('my_decorator_path', new=bypass_decorator):
        从 my_module 导入 function_to_test
    类 MyTestCase(unittest.TestCase):
        def test_function(自我):
            结果 = function_to_test()
            self.assertEqual(结果,预期结果)
    

3.正确构建测试

   import unittest
   from unittest.mock import patch
   class MyTestCase(unittest.TestCase):
       @patch('my_decorator_path', new=bypass_decorator)
       def test_function(self):
           from my_module import function_to_test
           result = function_to_test()
           self.assertEqual(result, expected_result)

4.运行测试

  1. Define the Mock Decorator

    def bypass_decorator(func):
         return func
    
  2. Apply the patch before importing the module

    import unittest
    from unittest.mock import patch
    with patch('my_decorator_path', new=bypass_decorator):
        from my_module import function_to_test
    class MyTestCase(unittest.TestCase):
        def test_function(self):
            result = function_to_test()
            self.assertEqual(result, expected_result)
    

3.Structure your tests properly

   import unittest
   from unittest.mock import patch
   class MyTestCase(unittest.TestCase):
       @patch('my_decorator_path', new=bypass_decorator)
       def test_function(self):
           from my_module import function_to_test
           result = function_to_test()
           self.assertEqual(result, expected_result)

4.Run your tests

请叫√我孤独 2024-12-15 09:44:53

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

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.

表情可笑 2024-12-15 09:44:53

那些搞乱内置导入的人会发现用 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.

平定天下 2024-12-15 09:44:53

简而言之:

def remove_decorators(obj):
    for attr_name in dir(obj):
        # attr_name.startswith('func_name_pattern') if you only need a specific function
        if callable(getattr(obj, attr_name)):
            while hasattr(getattr(obj, attr_name), '__wrapped__'):
                setattr(obj, attr_name, getattr(obj, attr_name).__wrapped__)



# this should be called before testing
remove_decorators(class)

请注意,它可能会删除所有装饰器,因此请随意检查其名称:)

In short:

def remove_decorators(obj):
    for attr_name in dir(obj):
        # attr_name.startswith('func_name_pattern') if you only need a specific function
        if callable(getattr(obj, attr_name)):
            while hasattr(getattr(obj, attr_name), '__wrapped__'):
                setattr(obj, attr_name, getattr(obj, attr_name).__wrapped__)



# this should be called before testing
remove_decorators(class)

Note that it might remove all decorators so feel free to check for its name :)

还不是爱你 2024-12-15 09:44:53

对我有用的方法基本上是取消装饰它。

我在 myapp.module_a 中有这个函数

from retrying import retry

@retry(stop_max_attempt_number=5)
def foo():
  print("something")

在这里尝试了其他方法(对我来说它们有意义),但他们没有使该函数不重试测试。

对我有用的是将以下代码放在 tests/__init__.py 中,如果你想为所有测试修补它,在你想要禁用它之前

from myapp import module_a

module_a.foo = getattr(module_a.foo, '__wrapped__', module_a.foo)

The way that worked for me was basically to undecorate it.

I have this function in myapp.module_a

from retrying import retry

@retry(stop_max_attempt_number=5)
def foo():
  print("something")

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

from myapp import module_a

module_a.foo = getattr(module_a.foo, '__wrapped__', module_a.foo)
心清如水 2024-12-15 09:44:53

我喜欢让技巧变得更简单、更容易理解。利用装饰器的功能并创建旁路。

模拟函数:

from functools import wraps

def the_call(*args, **kwargs):
    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            if kwargs.pop("bypass", None) is True:
                return function(*args, **kwargs)
            # You will probably do something that will change the response or the arguments here below
            args = ("bar")
            kwargs = {"stuff": "bar"}
            return function(*args, **kwargs)
        return wrapper
    return decorator

带有装饰器的函数:

@the_call()
def my_simple_function(stuff: str):
    return stuff


print(my_simple_function(stuff="Hello World"))

将返回:

“栏”

在你的测试中,只要传递参数bypass = True

print(my_simple_function(stuff="Hello World", 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:

from functools import wraps

def the_call(*args, **kwargs):
    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            if kwargs.pop("bypass", None) is True:
                return function(*args, **kwargs)
            # You will probably do something that will change the response or the arguments here below
            args = ("bar")
            kwargs = {"stuff": "bar"}
            return function(*args, **kwargs)
        return wrapper
    return decorator

Your function with the decorator:

@the_call()
def my_simple_function(stuff: str):
    return stuff


print(my_simple_function(stuff="Hello World"))

Will return:

"bar"

So in your tests, simply pass the parameter bypass = True

print(my_simple_function(stuff="Hello World", bypass=True))

Will return:

"Hello World"

神回复 2024-12-15 09:44:53

对于@lru_cache(max_size = 1000)


class MockedLruCache(object):

def __init__(self, maxsize=0, timeout=0):
    pass

def __call__(self, func):
    return func

缓存.LruCache = MockedLruCache

如果使用没有参数的装饰器,你应该:

def MockAuthenticated(func):
    return func

从龙卷风导入网络 web.authenticated = MockAuthenticated

for @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

if use decorator which haven't params, you should:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文