繁体   English   中英

如何在 Tkinter 应用程序上运行单元测试?

[英]How do I run unittest on a Tkinter app?

我刚刚开始学习TDD ,并且正在使用 Tkinter GUI 开发程序。 唯一的问题是,一旦调用了.mainloop()方法,测试套件就会挂起,直到窗口关闭。

这是我的代码示例:

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

测试 Tkinter 应用程序的适当方法是什么? 或者只是“不要”?

底线:在导致 UI 事件的操作之后,在需要该事件影响的后续操作之前,使用以下代码泵送事件。


IPython 提供了一个没有线程的优雅解决方案,它的gui tk魔术命令实现位于terminal/pt_inputhooks/tk.py

它不是root.mainloop() ,而是在循环中运行root.dooneevent() ,检查每次迭代的退出条件(交互式输入到达)。 这样,当 IPython 忙于处理命令时,偶数循环不会运行。

对于测试,没有要等待的外部事件,并且测试总是“忙碌”的,因此必须在“适当的时刻”手动(或半自动)运行循环。 他们是什么?

测试表明,如果没有事件循环,可以直接更改小部件(使用<widget>.tk.call()和任何包装它的东西),但事件处理程序永远不会触发。 因此,只要事件发生并且我们需要它的效果,就需要运行循环——即在任何更改某些内容的操作之后,在需要更改结果的操作之前。

源自上述 IPython 过程的代码将是:

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

这将处理(执行处理程序)所有未决事件,以及直接由这些事件产生的所有事件。

tkinter.Tk.dooneevent()委托给Tcl_DoOneEvent() 。)


作为旁注,使用它来代替:

root.update()
root.update_idletasks()

不一定会做同样的事情,因为这两个函数不会处理所有类型的事件。 由于每个处理程序都可能生成其他任意事件,因此我无法确定是否已经处理了所有事件。


这是一个测试用于编辑字符串值的简单弹出对话框的示例:

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

您可以做的一件事是在单独的线程中生成主循环并使用主线程运行实际测试; 按原样观看主循环线程。 确保在执行断言之前检查 Tk 窗口的状态。

多线程任何代码都很难。 您可能希望将 Tk 程序分解为可测试的部分,而不是一次对整个程序进行单元测试(这实际上不是单元测试)。

我最终建议至少在控制级别进行测试,如果不是较低的程序,它将极大地帮助您。

有一种称为monkey-patching 的技术,可以在运行时更改代码。

您可以对 TK 类进行猴子补丁,这样 mainloop 就不会真正启动程序。

在你的 test.py 中有这样的东西(未经测试!):

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

mock这样的 mocking 框架让这变得不那么痛苦。

这个答案适用于 Python 3.7 及更高版本(任何版本都有异步方法)

在您的main.py或您开始主 UI 的任何地方:

def start_application() -> Application:
    root = tk.Tk()
    app = Application(master=root)
    app.load_settings()
    return app # will return the application without starting the main loop.

if __name__=='__main__':
    start_application().mainloop()

在你的tests.py

from myapp.main import start_application

class TestGui(unittest.TestCase):
    
    # this will run on a separate thread.
    async def _start_app(self):
        self.app.mainloop()
    
    def setUp(self):
        self.app = start_application()
        self._start_app()
    
    def tearDown(self):
        self.app.destroy()
    
    def test_startup(self):
        title = self.app.winfo_toplevel().title()
        expected = 'The Application My Boss Wants Me To Make'
        self.assertEqual(title, expected)

这不会显示任何东西,但它会通过。 此外,预计会显示一条警告,说明我们没有等待_start_application 在这种情况下可以忽略这一点。 (如果您想成为多线程的忠实拥护者,那么您将不得不进行自己的线程管理……恕我直言,单元测试工作量太大了)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM