'Simulate the click on a button in the PyQt5 QMessageBox widget, during unittest CI

Rather than a long speech, if we run the minimum example below:

$ python3
Python 3.7.6 (default, Jan 30 2020, 09:44:41) 
[GCC 9.2.1 20190827 (Red Hat 9.2.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import unittest import sys
>>> from PyQt5.QtWidgets import QMessageBox, QApplication
>>> import unittest
>>>
>>> class Fortest():
...     def messagebox(self):
...         app = QApplication(sys.argv)
...         msg = QMessageBox()
...         msg.setIcon(QMessageBox.Warning)
...         msg.setText("message text")
...         msg.setStandardButtons(QMessageBox.Close)
...         msg.buttonClicked.connect(msg.close)
...         msg.exec()
... 
>>> class Test(unittest.TestCase):
...     def testMessagebox(self):
...         a=Fortest()
...         a.messagebox()
... 
>>> unittest(Test().testMessagebox())

we stay stuck with the widget asking to click on the Close button. This is not compatible with continuous integration unit tests ...

How to simulate the click on the close button in the test code (class Test), without changing the code to be tested (class Fortest) ?



Solution 1:[1]

The logic:

  • Get the QMessageBox, in this you can use QApplication::activeWindow().

  • Obtain the QPushButton using the button() method of QMessageBox.

  • Click with the mouseClick() method of the QTest sub-module.

But the above must be done an instant after the QMessageBox is displayed, and for this a delay must be done (in this case you can use threading.Timer()).

import sys

import unittest
import threading

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QApplication
from PyQt5.QtTest import QTest


class Fortest:
    def messagebox(self):
        app = QApplication(sys.argv)
        msg = QMessageBox()
        msg.setIcon(QMessageBox.Warning)
        msg.setText("message text")
        msg.setStandardButtons(QMessageBox.Close)
        msg.buttonClicked.connect(msg.close)
        msg.exec_()


class Test(unittest.TestCase):
    def testMessagebox(self):
        a = Fortest()
        threading.Timer(1, self.execute_click).start()
        a.messagebox()

    def execute_click(self):
        w = QApplication.activeWindow()
        if isinstance(w, QMessageBox):
            close_button = w.button(QMessageBox.Close)
            QTest.mouseClick(close_button, Qt.LeftButton)

Solution 2:[2]

eyllanesc's answer may well work, although I suspect there is a danger of test leakage, i.e. confusing errors cropping up in subsequent tests due to the use of another thread (threading.Timer).

There are two simpler solutions to this, although they are not ideal (for purists) in that you don't actually get to do a qtbot.keyPress(messagebox, QtCore.Qt.Key_Enter).

First: simple patch:

with mock.patch('PyQt5.QtWidgets.QMessageBox.question', return_value=QtWidgets.QMessageBox.No):
    qtbot.keyPress(tasks_treeview, QtCore.Qt.Key_Delete)

... where pressing the Delete key is hooked up to the process of deleting something, but a message box of confirmation is then made to pop up.

Second: monkeypatch. This is from the pytest-qt "manual", p. 27:

def test_mytest(qtbot, monkeypatch):
    ...
    monkeypatch.setattr(QtWidgets.QMessageBox, "question", lambda *args: QtWidgets.QMessageBox.Yes)
    qtbot.keyPress(tasks_treeview, QtCore.Qt.Key_Delete)

NB the monkeypatch fixture seems to come for free with pytest: you don't have to import it or anything.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 eyllanesc
Solution 2 mike rodent