In a subclass of QFileDialog
there is a method, on_dir_entered
, which should be called when the QFileDialog
's signal directoryEntered
fires, thus:
self.directoryEntered.connect(self.on_dir_entered)
The problem is that a signal takes a non-negligible time to take effect. Originally I was inspired by this answer by eyllanesc, a notable PyQt5 expert. With an isolated test this sort of technique using QTimer.singleShot()
can work, although I had vague doubts about it from the beginning. And indeed, it turns out on my machine that I get "test leakage" with this sort of thing, particularly when there is more than one such test method: strange errors apparently occurring outside the tests themselves:
TEARDOWN ERROR: Exceptions caught in Qt event loop:
... so I went back to the pytest-qt docs and found that there are various methods available beginning wait...
seemingly to cater to the problem of signals or other events taking a non-negligible time to have effect. So I made a few tries to test signal 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()
and then
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()
and then
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()
All fail: the method is called 0 times.
I also tried:
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)
... this times out (default 5000 ms). I have double-checked and triple-checked that the code setting up connect
on this signal is executed.
Later
By putting a print
statement in the called slot (on_dir_entered
) I now see what the problem is: despite the with mock.patch...
line, the method is not being mocked!
At my low level of knowledge of mocking etc. I am tending to assume that this is because of the fact of using a signal with emit()
to trigger the event: I can't think of another explanation.
NB this signal is fired "naturally" by one or two events in a QFileDialog
(such as clicking the "go to parent directory" QToolButton
). Maybe you have to do it that way... So I tried this:
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)
Time out. 0 calls. Again I was able to ascertain that the real method is being called, and the patch is not working.
What am I doing wrong and is there a way to test this with something from pytest-qt?
I think I may have found the answer. Experts in pytest-qt may like to comment.
I think I worked out (this is probably very obvious) that the problem in the above attempts using qtbot.waitUntil()
was that the patch I had set up no longer applied once the execution left the context manager block, and the call to the method was, indeed, delayed, which was the whole point.
A whole-test-method decorator patch is therefore one approach (or else retain the indent after setting up the patch context manager...). I found that both the following passed, and neither was a false positive (i.e. they failed if I commented out the app code line setting up the 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)
Update
It turns out that using qtbot.waitUntil
is probably not necessary, and such tests can usually pass by using QCoreApplication.processEvents()
. Thus the second test above would be rewritten:
@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()