Как запустить unittest в приложении Tkinter?

Я только начал изучать о TDD, и я разрабатываю программу с использованием графического интерфейса Tkinter. Единственная проблема в том, что однажды .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? Или это просто "нет"?

3 ответов


одна вещь, которую вы можете сделать, это создать mainloop в отдельном потоке и использовать свой основной поток для запуска фактических тестов; смотреть поток mainloop, как это было. Убедитесь, что вы проверяете состояние окна Tk, прежде чем делать ваши утверждения.

многопоточность любой код сложно. Вы можете разбить свою программу Tk на тестируемые части вместо модульного тестирования всего сразу (что на самом деле не является модульным тестированием).

Я бы, наконец, предложил тестирование по крайней мере на контрольный уровень если не ниже для вашей программы, это поможет вам чрезвычайно.


итог: перекачивайте события с приведенным ниже кодом после действия, которое вызывает событие пользовательского интерфейса, перед более поздним действием, которое нуждается в эффекте этого события.


IPython обеспечивает элегантное решение без потоков его gui tk реализация команды magic, которая находится в 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"йцу")  # the class implementing the dialog;
                                                  # not included in the example
        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()

существует метод, называемый 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 делает это намного менее болезненным.