How do I run unittest on a Tkinter app?

后端 未结 3 2102
眼角桃花
眼角桃花 2020-12-05 00:44

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

相关标签:
3条回答
  • 2020-12-05 01:17

    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()
    
    0 讨论(0)
  • 2020-12-05 01:20

    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.

    0 讨论(0)
  • 2020-12-05 01:23

    There is a technique called monkey-patching, whereby you change code at runtime.

    You could monkey-patch the TK class, so that mainloop doesn't actually start the program.

    Something like this in your test.py (untested!):

    import tk
    class FakeTk(object):
        def mainloop(self):
            pass
    
    tk.__dict__['Tk'] = FakeTk
    import server
    
    def test_server():
        s = server.Server()
        server.mainloop() # shouldn't endless loop on you now...
    

    A mocking framework like mock makes this a lot less painful.

    0 讨论(0)
提交回复
热议问题