Как запустить unittest в приложении Tkinter? - PullRequest
18 голосов
/ 03 ноября 2010

Я только начал изучать 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 ]

10 голосов
/ 28 февраля 2018

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


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"йцу")  # 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()
2 голосов
/ 03 ноября 2010

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

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

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

2 голосов
/ 03 ноября 2010

Существует метод, называемый monkey-patching, при котором вы изменяете код во время выполнения.

Вы можете monkey-patch для класса 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 делает это намного менее болезненным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...