Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
156 views
in Technique[技术] by (71.8m points)

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...