我似乎对此有所了解。重要的是,测试应该始终保持找到的东西......但这里只有一个其他答案似乎解决了这一点:如果你用模拟或假货代替真正的装饰器,你必须恢复那个真正的装饰器测试后。
在模块 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
在实践中,就我而言,在大多数情况下,对于所有测试,在每次运行开始时使用“无操作装饰器”完全修补这个装饰器会更有意义。但是为了说明如何在每个测试的基础上完成它,请参见下文。
提出的问题是is_thread_interrupt_req 类AbstractLongRunningTask 的方法(实际上它不是抽象的:您可以实例化它)必须在非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_effect 结合importlib.reload 来重新加载你的模块,它本身将使用装饰器。
注意这里的上下文管理器缩进:thread_check.thread_check 上的补丁只需要应用到reload... 在调用实际方法(is_thread_interrupt_req)时,假装饰器就位。
如果你不使用这个拆解装置restore_tm_classes,这里会发生一些非常奇怪的事情:事实上,在下一个测试方法中,然后(根据我的实验)看起来装饰器既不是真实的也不是do_nothing_decorator,正如我通过在两者中放入print 语句确定的那样。因此,如果您不通过重新加载调整后的模块来恢复,那么在测试套件期间,task_manager_classes 模块中的应用程序代码似乎会留下一个“僵尸装饰器”(似乎什么都不做)。
警告
在测试运行过程中使用importlib.reload 时会出现很大的潜在问题。
特别是它可以证明应用程序代码正在使用具有特定 id 值(即id(MyClass))的类 X,但测试代码(在此模块和随后运行的模块中)据称使用的是相同的类 X,但具有另一个id值!有时这可能无关紧要,有时它会导致一些相当莫名其妙的失败测试,这可能可以解决,但可能需要您解决
-
宁愿避免 mock.patching 在测试中实际未创建的对象:例如,当类本身时(我在这里不是在考虑 类的对象,而是 类作为变量本身)在任何测试之外导入或创建,因此在测试收集阶段创建:在这种情况下,类对象将和重装后的不一样。
-
甚至可以在以前没有此功能的各种模块中的一些固定装置中使用importlib.reload(...)!
始终使用pytest-random-order(多次运行)来揭示此类(和其他)问题的全部范围。
正如我所说,装饰器可以在运行开始时简单地修补。因此是否值得这样做是另一回事。事实上,我已经实现了相反的情况:thread_check 装饰器在运行开始时被修补,但随后 又被修补,使用上面的 @987654347 @ 技术,用于需要装饰器运行的一两个测试。