I've just begun learning about TDD, and I'm developing a program using a Tkinter GUI. The only problem is that once the .mainloop()
method is called, the test suite hangs until the window is closed.
Here is an example of my code:
# server.py import Tkinter as tk class Server(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.mainloop()
# test.py import unittest import server class ServerTestCase(unittest.TestCase): def testClassSetup(self): server.Server() # and of course I can't call any server.whatever functions here if __name__ == '__main__': unittest.main()
What is the appropriate way of testing Tkinter apps? Or is it just 'dont'?
The command to run the tests is python -m unittest filename.py . In our case, the command to run the tests is python -m unittest test_utils.py .
For a file module.py , the unit test should normally be called test_module.py , following Pythonic naming conventions. There are several commonly accepted places to put test_module.py : In the same directory as module.py . In ../tests/test_module.py (at the same level as the code directory).
pytest supports running Python unittest -based tests out of the box. It's meant for leveraging existing unittest -based test suites to use pytest as a test runner and also allow to incrementally adapt the test suite to take full advantage of pytest's features.
The Python standard library includes the unittest module to help you write and run tests for your Python code.
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()
One thing you can do is spawn the mainloop in a separate thread and use your main thread to run the actual tests; watch the mainloop thread as it were. Make sure you check the state of the Tk window before doing your asserts.
Multithreading any code is hard. You may want to break your Tk program down into testable pieces instead of unit testing the entire thing at once (which really isn't unit testing).
I would finally suggest testing at least at the control level if not lower for your program, it will help you tremendously.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With