I'm switching from PySide2 to PyQt5 and am getting a TypeError in one of my tests.
The test checks for whether exit_action
actually calls the close
method.
bug.py
# bug.py
from PySide2 import QtCore, QtWidgets, QtGui
# from PyQt5 import QtCore, QtWidgets, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.exit_action = QtWidgets.QAction('&Exit', self)
self.exit_action.triggered.connect(self.close)
self.file_menu = self.menuBar().addMenu('&File')
self.file_menu.addAction(self.exit_action)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
main_window = MainWindow()
main_window.show()
app.exec_()
test_bug.py
# test_bug.py
# python3 -m unittest test_bug.TestMainWindowExitAction
import bug
import unittest
import unittest.mock
from PySide2 import QtCore, QtWidgets, QtGui, QtTest
# from PyQt5 import QtCore, QtWidgets, QtGui, QtTest
if not QtWidgets.QApplication.instance():
app = QtWidgets.QApplication([])
class TestMainWindowExitAction(unittest.TestCase):
def setUp(self):
self.main_window = bug.MainWindow()
def test_exit_action_trigger_closes_application_with_mock(self):
close_mock = unittest.mock.MagicMock()
self.main_window.closeEvent = close_mock
self.main_window.exit_action.triggered.emit()
close_mock.assert_called_once()
# def test_exit_action_trigger_closes_application_without_mock(self):
# self.called=0
# def my_close(arg):
# print(f"HERE {arg}")
# self.called+=1
# self.main_window.closeEvent = my_close
# self.main_window.exit_action.triggered.emit()
# self.assertEqual(self.called, 1)
It works great with Pyside2. With PyQt5, it gives an error:
TypeError: invalid argument to sipBadCatcherResult()
Aborted
The error happens when exit_action.triggered
is emitted.
If I instead run test_exit_action_trigger_closes_application_without_mock
, everything runs fine:
HERE <PyQt5.QtGui.QCloseEvent object at 0x7f73488e75e0>
.
----------------------------------------------------------------------
Ran 1 test in 0.005s
OK
It's not clear to me what the "right" way to test this is. Even though the nested def works, it's not obvious how it works (although I do understand it). If I mock close
instead of closeEvent
, the mock isn't called during testing, despite the action closing the application when I manually run it. It's not clear to me if the problem is with PyQt5 (i.e. sip) or with unittest.mock
or if everything is fine and I've just confused myself.
If you add a return value to the mock then it should work (works on my machine at least)
close_mock = unittest.mock.MagicMock(return_value=None)
If you don't specify a return value then it will return another mock object which, I guess, upsets some type check within PyQt5/sip.