Как запустить 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().)