How do I run unittest on a Tkinter app?

Bottom line: pump the events with the below code after an action that causes a UI event, before a later action that needs the effect of that event.


IPython provides an elegant solution without threads it its gui tk magic command implementation that’s located in terminal/pt_inputhooks/tk.py.

Instead of root.mainloop(), it runs root.dooneevent() in a loop, checking for exit condition (an interactive input arriving) each iteration. This way, the even loop doesn’t run when IPython is busy processing a command.

With tests, there’s no external event to wait for, and the test is always “busy”, so one has to manually (or semi-automatically) run the loop at “appropriate moments”. What are they?

Testing shows that without an event loop, one can change the widgets directly (with <widget>.tk.call() and anything that wraps it), but event handlers never fire. So, the loop needs to be run whenever an event happens and we need its effect — i.e. after any operation that changes something, before an operation that needs the result of the change.

The code, derived from the aforementioned IPython procedure, would be:

def pump_events(root):
    while root.dooneevent(_tkinter.ALL_EVENTS|_tkinter.DONT_WAIT):
        pass

That would process (execute handlers for) all pending events, and all events that would directly result from those.

(tkinter.Tk.dooneevent() delegates to Tcl_DoOneEvent().)


As a side note, using this instead:

root.update()
root.update_idletasks()

would not necessarily do the same because neither function processes all kinds of events. Since every handler may generate other arbitrary events, this way, I can’t be sure that I’ve processed everything.


Here’s an example that tests a simple popup dialog for editing a string value:

class TKinterTestCase(unittest.TestCase):
    """These methods are going to be the same for every GUI test,
    so refactored them into a separate class
    """
    def setUp(self):
        self.root=tkinter.Tk()
        self.pump_events()

    def tearDown(self):
        if self.root:
            self.root.destroy()
            self.pump_events()

    def pump_events(self):
        while self.root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT):
            pass

class TestViewAskText(TKinterTestCase):
    def test_enter(self):
        v = View_AskText(self.root,value=u"йцу")
        self.pump_events()
        v.e.focus_set()
        v.e.insert(tkinter.END,u'кен')
        v.e.event_generate('<Return>')
        self.pump_events()

        self.assertRaises(tkinter.TclError, lambda: v.top.winfo_viewable())
        self.assertEqual(v.value,u'йцукен')


# ###########################################################
# The class being tested (normally, it's in a separate module
# and imported at the start of the test's file)
# ###########################################################

class View_AskText(object):
    def __init__(self, master, value=u""):
        self.value=None

        top = self.top = tkinter.Toplevel(master)
        top.grab_set()
        self.l = ttk.Label(top, text=u"Value:")
        self.l.pack()
        self.e = ttk.Entry(top)
        self.e.pack()
        self.b = ttk.Button(top, text="Ok", command=self.save)
        self.b.pack()

        if value: self.e.insert(0,value)
        self.e.focus_set()
        top.bind('<Return>', self.save)

    def save(self, *_):
        self.value = self.e.get()
        self.top.destroy()


if __name__ == '__main__':
    import unittest
    unittest.main()

Leave a Comment