Как применить pytest для приложения Tkinter?
Впервые пишу тест, и делаю это для созданного с помощью Tkinter игрового приложения. Его основной функционал: пользователь задает любые вопросы и получает случайно выбранные ответы из имеющегося списка. Ниже привожу краткий код, затем - вопрос по библиотеке pytest
import tkinter as tk
window = tk.Tk()
# создадим рамку, где будет поле для вопросов пользователя
frame_question_answer = tk.Frame(
master=window,
relief=tk.RIDGE,
borderwidth=5
)
# создадим рамку, которая формирует область с ответами на вопросы пользователя
frame_answer = tk.Frame(
master=window,
relief=tk.RIDGE,
borderwidth=5
)
# добавим виджет Text - многострочный текстовый бокс (в противовес однострочному Entry)
# Данная область предназначена для написания вопросов
text_box_question = tk.Text(
master=frame_question_answer_with_scroll,
width=67,
height=5,
bg='#EBF1F2',
fg='black'
)
# виджет для вывода случайных ответов
text_box_answer = tk.Text(
master = frame_answer_with_scroll,
width=48,
height=7.5,
bg='#EBF1F2',
fg='black'
)
# создадим элемент кнопки Спросить
button_ask = tk.Button(
master=frame_question_answer,
text='Спросить',
width=15,
height=2,
bg='#B533FF',
fg='yellow',
activebackground='#C4B1C3'
)
# создадим элемент кнопки Очистить
button_clear = tk.Button(
master=frame_question_answer,
text='Очистить',
width=15,
height=2,
bg='#B533FF',
fg='yellow',
activebackground='#C4B1C3'
)
frame_question_answer.grid(
row=0, column=0,
columnspan=1,
sticky='e'
)
frame_answer.grid(
row=0, column=1,
columnspan = 1, # дописал (этого нет в черновике): нормально - оставить
sticky='w'
)
text_box_question.grid(
row=0, column=0,
sticky='news'
)
text_box_answer.grid(
row=0, column=0,
sticky='news'
)
button_ask.grid(
row=3, column=0, sticky='ew'
)
button_clear.grid(
row=3, column=1, sticky='ew'
)
# Обработчик вопроса пользователя
def questionQuery(event):
question = text_box_question.get('1.0', tk.END) # читаем текст из виджета text_box_question
# time.sleep(2)
window.update_idletasks()
window.after(2000, lambda: button_ask.configure(text='Спросить'))
if not question.strip():
text_box_answer.insert(tk.END, 'Задайте свой вопрос\n')
else:
answerQuery()
# Функция возврата случайных ответов
def answerQuery():
time.sleep(2)
# window.after(2000, lambda: text_box_answer.insert(tk.END, 'Хмм...' + '\n')) # и выводим в виджет text_box_answer
text_box_answer.insert(tk.END, 'Хмм...' + '\n') # и выводим в виджет text_box_answer
window.update_idletasks()
time.sleep(2)
# window.after(2000, lambda: text_box_answer.insert(tk.END, 'Я заглядываю за повороты судьбы, чтобы узнать о грядущем' + '\n'))
text_box_answer.insert(tk.END, 'Я заглядываю за повороты судьбы, чтобы узнать о грядущем' + '\n')
window.update_idletasks()
window.after(2000, lambda: text_box_answer.insert(tk.END, random.choice(responses) + '\n'))
# функция очистки полей вопроса и ответа
def clearText(event):
time.sleep(1)
text_box_question.delete('1.0','end')
time.sleep(1)
text_box_answer.delete('1.0', tk.END)
button_ask.bind('<Button>', questionQuery)
# привязываем команду к кнопке очистки полей вопроса и ответа
button_clear.bind('<Button>', clearText)
window.mainloop()
python
Теперь о pytest. Необходимо протестировать обе представленные функции. Для этого я создаю тестовый модуль, импортирую в него функцию и... Это открывает окно программы, но затем тест встает
from Magic_Ball_prog import questionQuery, answerQuery
def test_questionQuery():
questionQuery('так?') == 'так?'
def test_answerQuery():
pass
Какой же тест был бы корректен? Хочу узнать, как функция реагирует на задаваемый вопрос, на пустую строку, как очищает ответы по кнопке "Очистить"
Ответы (1 шт):
Пока напишу общие мысли по поводу тестирования GUI, позже постараюсь добавить какой-то минимальный пример
- Как минимум, проверки внутри теста должны выглядеть так:
assert questionQuery('так?') == 'так?', но это будет работать, только если функция что-то возвращает (у вас они ничего не возвращают, по сути возвращаютNone). У вас в процессе выполнения функции текст добавляется в текстовое поле, получается, что нужно будет из этого текстового поля текст получить, потом сравнить с предполагаемым результатом. Либо через mock (см. ниже) заменить методinsertтекстового поля, и проверять, с какими параметрами он был вызван. - Нужно будет добавить принудительное закрытие окна, чтобы тест когда-нибудь завершился (он у вас будет висеть пока выполняется метод
window.mainloop(), а он выполняется пока существует окно). - Возможно (но это не точно) придется переписать код через классы (классом нужно будет сделать главное окно, чтобы его можно было по запросу создавать/уничтожать)
- Не очень понятно, как тестировать функции, которые дают случайный результат, т.к. не понятно, с чем сравнивать. Как вариант, можно проверять по шаблону (начало текста - такое-то, конец - из такого-то списка).
- Немного проблематично тестировать, когда результат записывается через функцию обратного вызова (функцию, которая передается в метод
afterвторым параметром). Как вариант, можно через mock (см. статью Модуль Mock: макеты-пустышки в тестировании (статья довольно старая, импорты могут отличаться, но для ознакомления пойдет)) заменить методafterна свою функцию, которая будет перехватывать что передается вторым параметром, и вызывать вручную без задержки. - Вообще, можно заменить на mock все элементы управления, и само окно, потом только проверять, сколько раз с какими параметрами был вызван какой метод. В этом случае можно уничтожить настоящее окно сразу после создания, и дальше работать только со своими тестовыми объектами.