Как запустить 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? Или это просто "не"?
Спасибо!
Ответы
Ответ 1
Одна вещь, которую вы можете сделать, - это создать mainloop в отдельном потоке и использовать свой основной поток для запуска реальных тестов; наблюдайте за потолком mainloop. Перед выполнением утверждений убедитесь, что вы проверяете состояние окна Tk.
Многопоточность любого кода сложна. Возможно, вы захотите разбить свою программу Tk на тестируемые части, а не на единицу, проверяя всю вещь сразу (это действительно не модульное тестирование).
Я бы, наконец, предложил тестирование по крайней мере на уровне управления, если не ниже для вашей программы, это очень поможет вам.
Ответ 2
Существует метод, называемый "патчем обезьяны", в котором вы меняете код во время выполнения.
Вы можете обезвредить класс TK, чтобы mainloop фактически не запускал программу.
Что-то вроде этого в 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...
Издевательская структура, такая как mock, делает это намного менее болезненным.
Ответ 3
@Ryan Ginstrom ответ упоминает макет. Этот рецепт Active State показывает, как макет избегает проблемы OP из висячего набора тестов: Надежное Unittesting элементов меню Tkinter с Mocking. Комплект тестов в этом рецепт не включает вызов mainloop
.
Ответ 4
Нижняя строка: накачивайте события с помощью приведенного ниже кода после действия, которое вызывает событие пользовательского интерфейса, до более позднего действия, которое нуждается в эффекте этого события.
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()
.)