Search code examples
pythonpyqt5python-unittest

Mocking a Qt slot with unittest


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.


Solution

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