I've started to create UI tests for my PyQt5 widgets using QtTest but have run into the following difficulties:
In order to speed up things, some of my widgets only perform operations when visible. As it seems that QtTest runs with invisible widgets, the corresponding tests fail.
For the same reason, I cannot test program logic that makes a subwidget visible under certain conditions.
Is there a way to make widgets visible during test? Is this good practice (e.g. w.r.t. CI test on GitHub) and is QtTest the way to go?
I have tried to use pytest with pytest-qt without success as I couldn't find a proper introduction or tutorial and I do know "Test PyQt GUIs with QTest and unittest".
Below you find a MWE consisting of a widget mwe_qt_widget.MyWidget
with a combobox, a pushbutton and a label that gets updated by the other two subwidgets:
from PyQt5.QtWidgets import QComboBox, QWidget, QPushButton, QLabel, QHBoxLayout, QApplication
class MyWidget(QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
self.n = 0 # click counter
self.lbl = QLabel("default", self)
self.but = QPushButton("+ 1", self)
self.cmb = QComboBox(self)
self.cmb.addItems(["A", "B", "C"])
lay_h_main = QHBoxLayout(self)
lay_h_main.addWidget(self.cmb)
lay_h_main.addWidget(self.but)
lay_h_main.addWidget(self.lbl)
self.setLayout(lay_h_main)
self.cmb.currentIndexChanged.connect(
lambda: self.lbl.setText(self.cmb.currentText()+f" {self.n}"))
self.but.clicked.connect(self.update_label)
# --------------------------------------------------------------------------
def update_label(self):
"""count clicks and update label with current combobox text"""
if self.isVisible():
self.n += 1
self.lbl.setText(self.cmb.currentText()+f" {self.n}")
# ==============================================================================
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainw = MyWidget(None)
app.setActiveWindow(mainw)
mainw.show()
sys.exit(app.exec_())
This widget is tested with the following test setup. test_visibility()
and test_button()
fail because both require that the widget-under-test is visible:
import sys, unittest
import mwe_qt_widget
from PyQt5 import QtTest, QtCore
from PyQt5.QtWidgets import QApplication
class WidgetTest(unittest.TestCase):
def init(self):
"""Instantiate widget-under-test and assert default settings"""
self.form = mwe_qt_widget.MyWidget()
self.assertEqual(self.form.cmb.currentText(), "A")
self.assertEqual(self.form.lbl.text(), "default")
# --------------------------------------------------------------------------
def test_button(self):
"""Test whether button click updates label"""
self.init()
QtTest.QTest.mouseClick(self.form.but, QtCore.Qt.LeftButton)
self.assertEqual(self.form.cmb.currentText(), "A")
self.assertEqual(self.form.lbl.text(), "A 1")
# --------------------------------------------------------------------------
def test_combobox(self):
"""Test whether combobox updates label"""
self.init()
QtTest.QTest.keyClick(self.form.cmb, QtCore.Qt.Key_PageDown)
QtTest.QTest.qWait(100)
self.assertEqual(self.form.cmb.currentText(), "B")
self.assertEqual(self.form.lbl.text(), "B 0")
# --------------------------------------------------------------------------
def test_visibility(self):
"""Test visibility of widget and subwidgets"""
self.init()
self.assertEqual(self.form.isVisible(), True)
self.assertEqual(self.form.cmb.isVisible(), True)
# ==============================================================================
if __name__ == '__main__':
app = QApplication(sys.argv) # Must construct a QApplication before a QWidget
unittest.main()
mainw = WidgetTest()
app.setActiveWindow(mainw)
mainw.show()
The problem is simple: QWidgets are hidden by default so isVisible() will return false, the solution is to invoke the show() method in init() to make it visible:
class WidgetTest(unittest.TestCase):
def init(self):
"""Instantiate widget-under-test and assert default settings"""
self.form = mwe_qt_widget.MyWidget()
self.form.show()
self.assertEqual(self.form.cmb.currentText(), "A")
self.assertEqual(self.form.lbl.text(), "default")