PyQt is the Python binding of the popular Qt cross-platform UI and application framework. For unit testing, PyQt clients are expected to use the standard Python unittest module, with just a little help from the PyQt QtTest module. It is not immediately apparent how to blend these two frameworks into a unified unit testing strategy. In this article, I show you how to unit test a PyQt GUI dialog using only modules included in PyQt and Python.
While the Qt C++ API includes a complete unit testing framework,The PyQt QtTest module contains only the QTest class, with static methods to simulate keystrokes, mouse clicks, and mouse movement.
Testing a GUI dialog requires only the keystroke methods to type strings into QLineEdit widgets, and mouse clicks to click the OK button. In more sophisticated drawing or layout applications, the mouse click and movement methods can be used to simulate drawing or dragging gestures.
The Margarita Mixer Dialog
For this example I used Qt Designer to create the user interface for a cocktail mixing machine. To avoid temptation, I chose margaritas because I do not like them so much.

In the upper portion of the dialog, the user specifies the number of jiggers for each ingredient (1 jigger = 0.0444 liters). In the lower portion, the user selects from blender speeds with names even stranger than those appearing on a real Oster Galaxie Cyclomatic. After specifying the amounts and blender speed, the user clicks OK, and an as-yet unimplemented machine creates the refreshing product.
PyQt QTest Example Files
The example is available on the Voom public repository, managed by Fog Creek Software‘s Kiln, which is based on the Mercurial version control system.
You have several options for getting the files. You can clone all the files on your computer using the Mercurial command,
hg clone https://voom.kilnhg.com/Repo/Make-Stuff-Happen/Group/PyQtTestExample
Or you can browse the file hierarchy. You can also click on the individual file names appearing in the list below.
The files are in directory PyQtTestExample/src/:
MargaritaMixer.uiis the XML output of Qt Designer. It describes the design of the GUI dialog.Ui_MargaritaMixer.pyis the Python source code file that describes the design of the GUI dialog. It is created from the above Qt Designer output file using the command:pyuic4 --output Ui_MargaritaMixer.py MargaritaMixer.ui
MargaritaMixer.pycontains the class that instantiates the GUI dialog and processes the resultsMargaritaMixerTest.pyis the unit test
Unit Test Program
This article is all about the unit test in file MargaritaMixerTest.py.
Imports
First import the required modules and classes, including of course the module under test, MargaritaMixer:
import sys import unittest from PyQt4.QtGui import QApplication from PyQt4.QtTest import QTest from PyQt4.QtCore import Qt import MargaritaMixer
Test the Dialog Defaults
The first test checks each of the default values of the dialog, pushes the OK button, and checks the volume returned by getJiggers():
def test_defaults(self):
self.assertEqual(self.form.ui.tequilaScrollBar.value(), 8 )
self.assertEqual(self.form.ui.tripleSecSpinBox.value(), 4)
self.assertEqual(self.form.ui.limeJuiceLineEdit.text(), "12.0")
self.assertEqual(self.form.ui.iceHorizontalSlider.value(), 12)
self.assertEqual(self.form.ui.speedButtonGroup.checkedButton().text(), "&Karate Chop")
# Class is in the default state even without pressing OK
self.assertEqual(self.form.getJiggers(), 36.0)
self.assertEqual(self.form.getSpeedName(), "&Karate Chop")
# Push OK with the left mouse button
okWidget = self.form.ui.buttonBox.button(self.form.ui.buttonBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
self.assertEqual(self.form.getJiggers(), 36.0)
self.assertEqual(self.form.getSpeedName(), "&Karate Chop")
Test PyQt QScrollBar
To test whether each ingredient appears in the total volume returned by getJiggers(), the test sets all ingredients to zero, sets the ingredient under test to some nonzero value, then calls getJiggers().
For convenience setFormToZero() sets all fields to zero:
def setFormToZero(self):
self.form.ui.tequilaScrollBar.setValue(0)
self.form.ui.tripleSecSpinBox.setValue(0)
self.form.ui.limeJuiceLineEdit.setText("0.0")
self.form.ui.iceHorizontalSlider.setValue(0)
Next test the scroll bar that determines the number of jiggers of tequila. Test the minimum and maximum values and then try a legal value:
def test_tequilaScrollBar(self):
self.setFormToZero()
# Test the maximum. This one goes to 11.
self.form.ui.tequilaScrollBar.setValue(12)
self.assertEqual(self.form.ui.tequilaScrollBar.value(), 11)
# Test the minimum of zero.
self.form.ui.tequilaScrollBar.setValue(-1)
self.assertEqual(self.form.ui.tequilaScrollBar.value(), 0)
self.form.ui.tequilaScrollBar.setValue(5)
# Push OK with the left mouse button
okWidget = self.form.ui.buttonBox.button(self.form.ui.buttonBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
self.assertEqual(self.form.getJiggers(), 5)
Test PyQt QDialogButtonBox
Note how in the previous and subsequent examples, QTest.mouseClick() is used to actually click on the center of the OK button:
okWidget = self.form.ui.buttonBox.button(self.form.ui.buttonBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
self.assertEqual(self.form.getJiggers(), 5)
Test PyQt QSpinBox
Next, set the triple sec spin box alone to a nonzero value and verify the result:
def test_tripleSecSpinBox(self):
self.setFormToZero()
self.form.ui.tripleSecSpinBox.setValue(2)
# Push OK with the left mouse button
okWidget = self.form.ui.buttonBox.button(self.form.ui.buttonBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
self.assertEqual(self.form.getJiggers(), 2)
Test PyQt QLineEdit
Use QTest.keyClicks() to actually type a string into the lime juice line edit widget:
def test_limeJuiceLineEdit(self):
self.setFormToZero()
# Clear and then type "3.5" into the lineEdit widget
self.form.ui.limeJuiceLineEdit.clear()
QTest.keyClicks(self.form.ui.limeJuiceLineEdit, "3.5")
# Push OK with the left mouse button
okWidget = self.form.ui.buttonBox.button(self.form.ui.buttonBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
self.assertEqual(self.form.getJiggers(), 3.5)
I used QTest.keyClicks() merely because this article emphasizes QtTest. I think the test would be just as valid if the widget text were set directly using QLineEdit.setText():
self.form.ui.limeJuiceLineEdit.setText("3.5")
Test PyQt QSlider
Test the ice slider:
def test_iceHorizontalSlider(self):
self.setFormToZero()
self.form.ui.iceHorizontalSlider.setValue(4)
# Push OK with the left mouse button
okWidget = self.form.ui.buttonBox.button(self.form.ui.buttonBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
self.assertEqual(self.form.getJiggers(), 4)
Test PyQt QRadioButton
The blender speed radio buttons are in a QButtonGroup. Test every button because it is very easy to leave one of them outside the button group:
def test_blenderSpeedButtons(self):
self.form.ui.speedButton1.click()
self.assertEqual(self.form.getSpeedName(), "&Mix")
self.form.ui.speedButton2.click()
self.assertEqual(self.form.getSpeedName(), "&Whip")
self.form.ui.speedButton3.click()
self.assertEqual(self.form.getSpeedName(), "&Puree")
self.form.ui.speedButton4.click()
self.assertEqual(self.form.getSpeedName(), "&Chop")
self.form.ui.speedButton5.click()
self.assertEqual(self.form.getSpeedName(), "&Karate Chop")
self.form.ui.speedButton6.click()
self.assertEqual(self.form.getSpeedName(), "&Beat")
self.form.ui.speedButton7.click()
self.assertEqual(self.form.getSpeedName(), "&Smash")
self.form.ui.speedButton8.click()
self.assertEqual(self.form.getSpeedName(), "&Liquefy")
self.form.ui.speedButton9.click()
self.assertEqual(self.form.getSpeedName(), "&Vaporize")
Comments, Please
If you have ideas on my style or ways to improve this article, please help everybody and leave a comment!
Photo credit: Oster Galaxie Cyclomatic by The Thrift Collective.

{ 6 comments… read them below or add one }
Is there a similar approach to testing GUIs written in PerlTk?
That’s a very different topic, Divyanand. I use
Test::Morefor Perl unit testing, but that’s all I know.The difficulties I had bringing up PyQt unit testing were strongly related to Python and QtTest, so I do not think that this article will help you with PerlTk.
[Note: The problem described here has been fixed--John McGehee]
Hi,
Testing your script I’ve got an error
AttributeError: ‘MargaritaMixer’ object has no attribute ‘jiggers’
Just change the function getLitters and it will be OK
def getLiters(self):
”’Return the total volume of the margaritas in liters.”’
return 0.0444 * self.getJiggers()
Thank’s for this interesting topic.
Cheers
Ah, you’re right, Vincent. I violated the paramount rule of unit testing: either test it or eliminate it. I knew the test for
getLiters()was missing, and it was bothering me–bothering me for good reason.I added a test and fixed the bug.
Thanks for writing this tutorial. GUI testing is something people ask about whenever the subject of testing comes up.
I found my way here from the PyQt Wiki. Do you think you could add a link to the http://www.diotavelli.net/PyQtWiki/GUI_Testing page, and maybe update your link from the http://www.diotavelli.net/PyQtWiki/SampleCode page to refer to a page on the Wiki itself? (The code linked to from there is currently unavailable and all the other pieces of sample code have their own pages on the Wiki.)
Thank you for your comment, David. I added a link to the GUI_Testing page, and just deleted my link from the sample code page. If you previously had trouble accessing the example code, that issue is now fixed; please try again.