如何检查对信号的响应

How to check response to a signal

QFileDialog 的子类中有一个方法,on_dir_entered,应该在 QFileDialog 的信号 directoryEntered 触发时调用,因此:

self.directoryEntered.connect(self.on_dir_entered)

问题是信号需要很长时间才能生效。最初我的灵感来自著名的 PyQt5 专家 eyllanesc 。通过独立测试,这种使用 QTimer.singleShot() 的技术可以工作,尽管我从一开始就对它有模糊的怀疑。事实上,在我的机器上我得到了这种东西的“测试泄漏”,特别是当有不止一种这样的测试方法时:奇怪的错误显然发生在测试本身之外:

TEARDOWN ERROR: Exceptions caught in Qt event loop:

...所以我回到了 pytest-qt 文档,发现有多种方法可用 wait... 似乎可以解决信号或其他事件花费不可忽略的时间的问题有作用。所以我做了几次尝试来测试信号 directoryEntered:

def test_directoryEntered_triggers_on_dir_entered(request, qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = save_project_dialog_class.SaveProjectDialog(project)
    with mock.patch.object(fd, 'on_dir_entered') as mock_entered:
        fd.directoryEntered.emit('dummy')
    qtbot.waitSignal(fd.directoryEntered, timeout=1000)
    mock_entered.assert_called_once()

然后

def test_directoryEntered_triggers_on_dir_entered(qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = SaveProjectDialog(project)
    qtbot.waitSignal(fd.directoryEntered, timeout=1000)
    with mock.patch.object(fd, 'on_dir_entered') as mock_entered:
        fd.directoryEntered.emit('dummy')
    mock_entered.assert_called_once()

然后

def test_directoryEntered_triggers_on_dir_entered(qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = SaveProjectDialog(project)
    with mock.patch.object(fd, 'on_dir_entered') as mock_entered:
        fd.directoryEntered.emit('dummy')
    qtbot.wait(1000)
    mock_entered.assert_called_once()

全部失败:该方法被调用0次。

我也试过:

def test_directoryEntered_triggers_on_dir_entered(qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = SaveProjectDialog(project)
    with mock.patch.object(fd, 'on_dir_entered') as mock_entered:
            fd.directoryEntered.emit('dummy')
    def check_called():
        mock_entered.assert_called_once()
    qtbot.waitUntil(check_called)

...这次超时(默认 5000 毫秒)。我已经仔细检查并三次检查了在此信号上设置 connect 的代码是否已执行。

稍后

通过在被调用的插槽 (on_dir_entered) 中放置一个 print 语句,我现在明白了问题所在:尽管有 with mock.patch... 行,但该方法没有被模拟!

以我对模拟等的了解程度较低。我倾向于假设这是因为使用带有 emit() 的信号来触发事件:我想不出另一种解释.

注意此信号由 QFileDialog 中的一个或两个事件“自然地”触发(例如单击“转到父目录”QToolButton)。也许你必须那样做......所以我试过这个:

def test_directoryEntered_triggers_on_dir_entered(request, qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = save_project_dialog_class.SaveProjectDialog(project)
    to_parent_button = fd.findChild(QtWidgets.QToolButton, 'toParentButton')
    print(f'qtbot {qtbot} type(qtbot) {type(qtbot)}')
    with mock.patch.object(SaveProjectDialog, 'on_dir_entered') as mock_entered:
        qtbot.mouseClick(to_parent_button, QtCore.Qt.LeftButton)
    def check_called():
        mock_entered.assert_called_once()
    qtbot.waitUntil(check_called, timeout=1000)

超时。 0 个电话。我再次能够确定正在调用真正的方法,并且补丁不起作用。

我做错了什么,有没有办法用 pytest-qt 的东西来测试它?

我想我可能已经找到了答案。 pytest-qt的高手不妨评论一下。

我想我已经解决了(这可能非常明显)上面使用 qtbot.waitUntil() 的尝试中的问题是,一旦执行离开上下文管理器块,我设置的补丁就不再应用,确实,对该方法的调用被延迟了,这就是重点。

因此,整体测试方法装饰器补丁是一种方法(或者在设置补丁上下文管理器后保留缩进...)。我发现以下两项都通过了,而且都不是误报(即,如果我注释掉设置 connect 的应用程序代码行,它们就会失败):

@mock.patch('SaveProjectDialog.on_dir_entered')
def test_directoryEntered_signal_triggers_on_dir_entered(mock_entered, request, qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = SaveProjectDialog(project)
    fd.directoryEntered.emit('dummy')
    def check_called():
        mock_entered.assert_called_once()
    qtbot.waitUntil(check_called, timeout=1000)
    
@mock.patch('SaveProjectDialog.on_dir_entered')
def test_on_click_toParentButton_triggers_on_dir_entered(mock_entered, request, qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = SaveProjectDialog(project)
    to_parent_button = fd.findChild(QtWidgets.QToolButton, 'toParentButton')
    qtbot.mouseClick(to_parent_button, QtCore.Qt.LeftButton)
    def check_called():
        mock_entered.assert_called_once()
    qtbot.waitUntil(check_called, timeout=1000)

更新
事实证明,使用qtbot.waitUntil可能是不必要的,使用QCoreApplication.processEvents()通常可以通过这样的测试。因此上面的第二个测试将被重写:

@mock.patch('SaveProjectDialog.on_dir_entered')
def test_on_click_toParentButton_triggers_on_dir_entered(mock_entered, request, qtbot, tmpdir):
    project = mock.Mock()
    project.main_window = QtWidgets.QWidget()
    project.home_dir_path = pathlib.Path(str(tmpdir))
    fd = SaveProjectDialog(project)
    to_parent_button = fd.findChild(QtWidgets.QToolButton, 'toParentButton')
    qtbot.mouseClick(to_parent_button, QtCore.Qt.LeftButton)
    QtCore.QCoreApplication.processEvents()
    mock_entered.assert_called_once()