This Blog continues on http://aliafshar.github.io/blog

Tuesday, March 13, 2007

Unit Testing PyGTK

Unit testing a GUI has always been something that scares me, and scares other people too.


Because of this there are about a gazillion tools that behave in different ways, for example in Python, just have a look at:

http://pycheesecake.org/wiki/PythonTestingToolsTaxonomy#GUITestingTools

(Since the whole world is obsessed with Web Applications, and this is becoming perversely true even in Python circles, some of these GUI Testing tools are actually web testing tools, but never mind that.)

How do I do it? Well, we may talk in another blog about one of those tools, the awesome hack that is kiwi.ui.test but it is slightly restrictive on what you want to do, and how you should do it. Specifically, it needs you to have every widget named (which is fine when testing Glade-created interfaces, but a pain when hand-building them).

Enter this one function that I found in the kiwi source. Kiwi is LGPL (whatever that means, but you should read the license if you are going to use it).


import time
import gtk

# Stolen from Kiwi
def refresh_gui(delay=0):
while gtk.events_pending():
gtk.main_iteration_do(block=False)
time.sleep(delay)


Simple, and tiny, and it has the effect of turning your awful asynchronous run-events-when-it-wants-to User Interface into a simple synchronous thing.

How? Well simply it runs all the events that are waiting in the gtk event queue, as if you ran gtk.main() for a short while until it was done with everything.

The optional delay argument is a short delay that might prove useful, but I have yet to actually use it.

Let's look at an example of how to use this in a unit test.


from unittest import TestCase, main
import gtk

class MyView(gtk.VBox):

def __init__(self):
super(MyView, self).__init__()
self._button = gtk.Button('Click Me')
self._label = gtk.Label()
self.pack_start(self._button)
self.pack_start(self._label)
self._count = 0
self._button.connect('clicked', self.on_button_clicked)

def on_button_clicked(self, button):
self._count = self._count + 1
self._label.set_text('clicked %s times' % self._count)

class MyViewTest(TestCase):

def setUp(self):
self._v = MyView()

def test_count(self):
self.assertEqual(self._v._count, 0)
self._v._button.clicked()
refresh_gui()
self.assertEqual(self._v._count, 1)

def test_label(self):
self._v._button.clicked()
refresh_gui()
self.assertEqual(self._v._label.get_text(), 'clicked 1 times')

if __name__ == '__main__':
main()



And we run it:


Ran 2 tests in 0.013s

OK


Great!

Things we should note:

  • We never had to put our custom widget into a window
  • We never had to display it to the outside world
  • We can be sure asynchronous things like signal callbacks have happened
  • We can treat our UI as a purely synchronous application
  • We use unittest.TestCase, we don't need any fancy UI testing framework's subclasses.
Some clever people might try this test without the refresh_gui call, and may even notice that the tests pass, but that is because it is extremely simple in this example. Try it with more complicated stuff that is scheduled in gobject's idle time, and your results will start to become inconsistent.

Conclusion: Keep it simple! In a GUI Testing War, I would hedge this function against anything.