I try to create an integration test for an graphical user interface (GUI) written with Qt5
through python3
(so using pyqt5
).
And I use pytest
with the plugging pytest-qt
to test the GUI.
I test the GUI which here is largely inspired from this question, so the command pytest -v -s
runs well.
Since my repository is on Github
, I use Travis-CI
to perform my integration tests.
However when I push on Github
and so I launch the Travis
tests I get at some point the following error:
Exceptions caught in Qt event loop:
________________________________________________________________________________
Traceback (most recent call last):
File "/home/travis/build/XXXX/Test/GUI_test.py", line 29, in handle_dialog
yes_button = messagebox.button(QtWidgets.QMessageBox.Yes)
AttributeError: 'Example' object has no attribute 'button'
I reproduce this error in a MWE with the following files that are contained in my git repository:
the GUI written in python GUI.py
:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject
from PyQt5.QtGui import QIcon
class Example(QMainWindow):
def __init__(self, parent = None):
super().__init__()
self.initUI(self)
def initUI(self, MainWindow):
# centralwidget
MainWindow.resize(346, 193)
self.centralwidget = QtWidgets.QWidget(MainWindow)
# The Action to quit
self.toolb_action_Exit = QAction(QIcon('exit.png'), 'Exit', self)
self.toolb_action_Exit.setShortcut('Ctrl+Q')
self.toolb_action_Exit.triggered.connect(self.close)
# The Button
self.btn_prt = QtWidgets.QPushButton(self.centralwidget)
self.btn_prt.setGeometry(QtCore.QRect(120, 20, 89, 25))
self.btn_prt.clicked.connect(lambda: self.doPrint() )
self.btn_quit = QtWidgets.QPushButton(self.centralwidget)
self.btn_quit.setGeometry(QtCore.QRect(220, 20, 89, 25))
self.btn_quit.clicked.connect(lambda: self.close() )
# The textEdit
self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
self.textEdit.setGeometry(QtCore.QRect(10, 60, 321, 81))
# Show the frame
MainWindow.setCentralWidget(self.centralwidget)
self.show()
def doPrint(self):
print('TEST doPrint')
def closeEvent(self, event):
# Ask a question before to quit.
self.replyClosing = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
if self.replyClosing == QMessageBox.Yes:
event.accept()
else:
event.ignore()
def main_GUI():
print('start')
app = QApplication(sys.argv)
imageViewer = Example()
return app, imageViewer
if __name__ == '__main__':
app, imageViewer =main_GUI()
rc= app.exec_()
print('App end is exit code {}'.format(rc))
sys.exit(rc)
the file use by pytest
to create unittest GUI_test.py
:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os, sys
from PyQt5 import QtGui, QtCore, QtWidgets, QtTest
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject
import pytest
import warnings
from pytestqt.plugin import QtBot, capture_exceptions
import mock
@pytest.fixture(scope="module")
def Viewer(request):
print(" SETUP GUI")
GUI= __import__('GUI')
app, imageViewer = GUI.main_GUI()
with capture_exceptions() as exceptions:
qtbotbis = QtBot(app)
QtTest.QTest.qWait(0.5 *1000)
yield app, imageViewer, qtbotbis
######### EXIT ##########
app.quitOnLastWindowClosed()
def handle_dialog():
messagebox = QtWidgets.QApplication.activeWindow()
yes_button = messagebox.button(QtWidgets.QMessageBox.Yes)
qtbotbis.mouseClick(yes_button, QtCore.Qt.LeftButton, delay=1)
QtCore.QTimer.singleShot(100, handle_dialog)
qtbotbis.mouseClick(imageViewer.btn_quit, QtCore.Qt.LeftButton, delay=1)
assert imageViewer.isHidden()
app.closeAllWindows()
app.quit()
app.exit()
app.closingDown()
QtTest.QTest.qWait(0.5 *1000)
with mock.patch.object(QApplication, "exit"):
app.exit()
assert QApplication.exit.call_count == 1
print("[Notice] So a mock.patch is used to count if the signal is emitted.")
print(" TEARDOWN GUI")
class Test_GUI_CXS() :
def test_buttons(self, Viewer, caplog):
app, mainWindow, qtbot = Viewer
qtbot.mouseClick( mainWindow.btn_prt, QtCore.Qt.LeftButton )
The file to control the travis job .travis.yml
(which is able to deal with graphical windows according to the documentation p32):
language: python
python:
- "3.7"
sudo: required
dist: bionic
jobs:
include:
- stage: test
name: PyTest-GUI
before_install:
- python -m pip install --upgrade pip
- pip install -r ./requirement.txt
- sudo apt-get install -y libdbus-1-3 libxkbcommon-x11-0 dzen2
install:
- "export DISPLAY=:99.0"
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac+extension GLX +render -noreset"
- sleep 3
before_script:
- "herbstluftwm &"
- sleep 1
script:
- pytest -s -v ./GUI_test.py
addons:
apt:
packages:
- x11-utils
- libxkbcommon-x11-0
- herbstluftwm
- xvfb
services: xvfb
and the file containning the required libraries requirement.txt
:
pyqt5
mock
pytest
pytest-qt
I try to run the travis job in a debug mode. So after connecting through a ssh
the install all the dependencies, I tryed to run the command pytest
and get the same error.
However, if I do herbstluftwm &
then pytest
the test runs well and no error appear.
Therefore, I assume that there is a problem with the command herbstluftwm &
on the normal travis job, but I do not know how to solve it.
Any tips or help is welcomed !
In my previous answer I chose 100ms empirically but depending on the resources that time may vary so that I don't have to place a time that can fail and implemented a function that will run every T seconds until I find the QMessageBox.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtWidgets, QtTest
import mock
import pytest
from pytestqt.plugin import QtBot, capture_exceptions
def get_messagebox(t=100, max_attemps=-1):
messagebox = None
attempt = 0
loop = QtCore.QEventLoop()
def on_timeout():
nonlocal attempt, messagebox
attempt += 1
active_window = QtWidgets.QApplication.activeWindow()
if isinstance(active_window, QtWidgets.QMessageBox):
messagebox = active_window
loop.quit()
elif max_attemps > 0:
if attempt > max_attemps:
loop.quit()
else:
QtCore.QTimer.singleShot(t, on_timeout)
QtCore.QTimer.singleShot(t, on_timeout)
loop.exec_()
return messagebox
@pytest.fixture(scope="module")
def Viewer(request):
print(" SETUP GUI")
GUI = __import__("GUI")
app, imageViewer = GUI.main_GUI()
with capture_exceptions():
qtbotbis = QtBot(app)
QtTest.QTest.qWait(0.5 * 1000)
yield app, imageViewer, qtbotbis
app.quitOnLastWindowClosed()
def handle_dialog():
messagebox = get_messagebox()
yes_button = messagebox.button(QtWidgets.QMessageBox.Yes)
qtbotbis.mouseClick(yes_button, QtCore.Qt.LeftButton, delay=1)
QtCore.QTimer.singleShot(10, handle_dialog)
qtbotbis.mouseClick(imageViewer.btn_quit, QtCore.Qt.LeftButton, delay=1)
assert imageViewer.isHidden()
app.closeAllWindows()
app.quit()
app.exit()
app.closingDown()
QtTest.QTest.qWait(0.5 * 1000)
with mock.patch.object(QtWidgets.QApplication, "exit"):
app.exit()
assert QtWidgets.QApplication.exit.call_count == 1
print("[Notice] So a mock.patch is used to count if the signal is emitted.")
print(" TEARDOWN GUI")
class Test_GUI_CXS:
def test_buttons(self, Viewer, caplog):
app, mainWindow, qtbot = Viewer
qtbot.mouseClick(mainWindow.btn_prt, QtCore.Qt.LeftButton)
On the other hand to do my test in travis only the following configuration is necessary:
language: python
python:
- "3.7"
dist: bionic
jobs:
include:
- stage: test
name: PyTest-GUI
before_script:
- python -m pip install --upgrade pip
- pip install -r ./requirement.txt
script:
- pytest -s -v ./GUI_test.py
addons:
apt:
packages:
- libxkbcommon-x11-0
services:
- xvfb