Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I run unittest on a Tkinter app?

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'?

like image 252
Wayne Werner Avatar asked Nov 03 '10 02:11

Wayne Werner


People also ask

How do I run unittest Python?

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 .

Where do I put unittest Python?

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).

Can I use unittest with Pytest?

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.

Does unittest come with Python?

The Python standard library includes the unittest module to help you write and run tests for your Python code.


2 Answers

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() 
like image 138
ivan_pozdeev Avatar answered Oct 06 '22 00:10

ivan_pozdeev


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.

like image 33
Scott Avatar answered Oct 05 '22 23:10

Scott