- PVSM.RU - https://www.pvsm.ru -
Продолжение рассказа о замечательном кроссплатформенном фрейворке для функционального-тестирования TextTest. Первая часть статьи. [1]
Сейчас подавляющее большинство инструментов тестирования GUI работают одним из двух способов:
Проблемы первых — неустойчивость тестов, стоит изменить взаимное расположение кнопок на форме, даже на пару пикселей, упадет куча тестов, изменится разрешение экрана — упадут все тесты, перешли на новую версию GUI-фрейморка, в которой, допустим, улучшили отрисовку шрифтов — опять же упадут все тесты. Для каждой операционной системы нужно делать свой набор тестов, возможно, все придется переделывать даже после установки очередного сервис-пака, если он затронул графическую подсистему.
Я не спорю внешний вид тестировать полезно, но это должны быть тесты только на оформление, тестировать через них логику работы GUI это издевательство над человеком.
У второго подхода другие проблемы — тесты пишутся долго и сложно, а в добавок они зачастую совершенно непонятны окружающим, особенно непрограммистам. Портянки кода вроде этого:
ActivateWindow("Unsaved Document 1 - gedit")
SetEdit(5, "5")
ClickButton("Save")
Wait(10)
VerifyLabel(2, 10)
не дают представления о том, что происходит и что собственно тестируется. Программисту нужно явно писать assert'ы, на то, что после допустим нажатия этой кнопки, вот это label изменил свое значение на такой, а вот этот — не изменил. Так же API операционной системы может не предоставлять полного доступа к внутренностям виджетов, а уж если это нестандартный виджет, то считать его свойства становится практически невозможно.
Зато тут уже легче тестировать логику работы GUI, мы не завязаны на графическое представление элементов, а имея хороший инструмент можно даже попытаться написать кроссплатформенные тесты.
И наконец, у обоих подходов есть общая проблема с тестированием не синхронных событий. Допустим, ввели в браузер URL, а когда проверять что страница загрузилась?
Можно делать таймаут, но тут проблема, что если страница в среднем загружается за 5 сек, то нужно «для верности» выставить таймаут раз в 5 больше, да и то не факт, что все отработает. Это приводит к замедлению тестов и к вероятностному характеру их выполнения.
Вариант второй — «заточится» на некоторые тайные виджеты и как только они примут нужное состояние, считать, что действие выполнилось. Это добавляет работы программисту и делает тесты еще более нечитабельными и непонятными.
Он для решения описанных выше проблем предлагает использовать библиотеку StoryText. Последняя перед запуском вашего приложения «оборачивает» интерфейсы обращения к библиотеке GUI, подсовывая программе свои варианты интерфейсов. Причем делается это совершенно прозрачно. В большинстве случаев, вам не придется менять ни единой строчки кода в тестируемом приложении. Это дает возможность при записи теста сохранять в лог действия пользователя и реакцию программы на эти действия. А при тестировании — повторять действия пользователя и сравнивать реакцию программы с эталонной.
Итак, что же это нам дает:
entry_in = FindEditByName("entry_in")
SetValueForEdit(entry_in, 5)
calc_async = FindButtonByName("calc_async")
SendEvent(calc_async, <Enter>)
SendEvent(calc_async, <Button-1>)
SendEvent(calc_async, <ButtonRelease-1>)
WaitEvent("data to be loaded")
exit = FindButtonByName("exit")
SendEvent(exit, <Enter>)
SendEvent(exit, <Button-1>)
SendEvent(exit, <ButtonRelease-1>)
что согласитесь мало приятно, а вот так:
enter_data 5
run_calc_async
wait for data to be loaded
exit_from_form
Где enter_data, run_calc_async и exit_from_form — пользовательские названия, которые он ввел после записи теста (значения алиасов — всегда можно посмотреть специальном файле настроек)
Согласитесь идеология — замечательная, тесты создаются быстро, удобно, читабельно. Есть конечно и проблемы, не с идеологией, а с реализацией. У автора, похоже основная библиотека для GUI это gtk, остальные добавлены в неком экспериментальном режиме и реализованы не полностью, однако ниже я покажу, насколько не сложно добавлять новую функциональность.
Тестировать будет класс TestGUI. Класс создает очень простую форму на которой предлагается ввести значение, и нажать одну из кнопок OnCalcSync или OnCalcAsync, первая выведет значение, умноженное на два сразу, вторая подождет 10 секунд и выведет тоже самое. Да и я заранее извиняюсь за ужасный вид формы, но это же только пример.
class TestGUI:
def __init__(self, root):
self.root = root
frame1 = Tkinter.Frame(self.root)
frame1.pack(fill="both")
frame2 = Tkinter.Frame(self.root)
frame2.pack(fill="both")
frame3 = Tkinter.Frame(self.root)
frame3.pack(fill="both")
frame4 = Tkinter.Frame(self.root)
frame4.pack(fill="both")
Tkinter.Label(frame1, text="Input:").pack(side="left")
self.var_in = Tkinter.StringVar(value="")
Tkinter.Entry(frame1, name="entry_in", textvariable=self.var_in).pack()
Tkinter.Label(frame2, text="Output:").pack(side="left")
self.label_out = Tkinter.Label(frame2, name="label_out")
self.label_out.pack(side="left")
Tkinter.Button(frame3, name="entry_calc_sync", text="OnCalcSync", width=15, command=self.on_press_sync).pack(side="left")
Tkinter.Button(frame4, name="entry_calc_async", text="OnCalcAsync", width=15, command=self.on_press_async).pack(side="left")
Tkinter.Button(frame4, name="entry_exit", text="OnExit", width=15, command=self.on_exit).pack(side="left")
self.root.title("Hello World!")
self.root.mainloop()
def _calc(self):
try:
return str(int(self.var_in.get()) * 2)
except:
return "error input"
def on_press_sync(self):
self.label_out["text"] = self._calc()
def _operation_finish(self):
storytext.applicationEvent('data to be loaded')
self.label_out["text"] = self._calc()
def on_press_async(self):
self.root.after(10 * 1000, self._operation_finish)
def on_exit(self):
self.root.destroy()
Хочу обратить внимание, что при запуске теста импортируется модуль tkinter_ex (скачать можно отсюда [3] и положить рядом с test.py). Он нужен т.к. поддержка tkinter в библиотеке все еще «Experimental and rather basic support for Tkinter» как пишет сам автор. В частности, стандартный модуль для tkinter совершенно не умеет отслеживать изменение текста у класса Label, но к счастью починить это совсем не сложно. Для этого мы подменяем стандартный Tkinter.Label на свой с сохранением исходного имени и функциональности, а в функциях которые могли бы изменить текст label — «configure» и "__setitem__" добавляем логирование вот такого вида: «Updated Text for label '%s' (set to %s)». Благодаря динамизму Python код получился не сложный
# -*- coding: utf-8 -*-
import Tkinter
import logging
origLabel = Tkinter.Label
class Label(origLabel):
def __init__(self, *args, **kw):
origLabel.__init__(self, *args, **kw)
self.logger = logging.getLogger("gui log")
def _update_text(self, value):
self.logger.info("Updated Text for label '%s' (set to %s)" % (self.winfo_name(), value))
def configure(self, *args, **kw):
origLabel.configure(self, *args, **kw)
if "text" in kw:
self._update_text(kw["text"])
def __setitem__(self, key, value):
origLabel.__setitem__(self, key, value)
if key == "text":
self._update_text(value)
config = configure
internal_configure = origLabel.configure
Tkinter.Label = Label
Итак, поехали: Добавляем новый test-suite «Suite_GUI» и тест к нему «Test_GUI_Sync» с параметром «gui». в simpletestsconfig.cfg добавляем настройки указывающие на то, что мы будем тестировать GUI на основе tkinter
# Mode for Use-case recording (GUI, console or disabled)
use_case_record_mode:GUI
# How long in seconds to wait between each GUI action
slow_motion_replay_speed:3.0
# Which Use-case recorder is being used
use_case_recorder:storytext
# Single program to use as interpreter for the SUT
interpreter:storytext -i tkinter
virtual_display_count:0
Расшифровывать все настройки не буду, про них можно почитать вот тут [4]. Обращу внимание только на одну: «virtual_display_count», она должна иметь смысл только на UNIX системах и позволяет запускать тесты на виртуальных дисплеях через Xvfb. Но из-за ошибки реализации если этот параметр не выставить, StoryText пытается создавать виртуальные дисплеи на Windows, где Xvfb отсутствует. Поэтому настройку нужно явно добавлять и выставлять в 0.
Не забываем перезагрузить IDE после внесения настроек. После чего в меню «Test_GUI_Sync» станет доступен пункт «Record Use-Case», запускаем, ничего в появившемся окне не меняем. Появляется наша тестовая неказистая форма:
в ней нужно выполнить действия, которые хотим оттестировать, вводим значение в поле «Input», нажимаем «OnCalcSync» и потом «Exit». После этого StoryText предложит нам дать человеческие имена для выполненных действий, заполним их, например вот так:
Как обычно сохраняем и пробуем запустить тест еще раз, все отлично отрабатывает.
Чтобы понять что произошло можно заглянуть в появившийся на диске: simpletestsstorytext_filesui_map.conf, где описаны алиасы, которые мы только что ввели. Так же стоит посмотреть в папке с тестом (Test_GUI_Sync) на файл usecase.cfg, в котором получившиеся действия описаны вполне человеческим языком:
enter_data 5
run_calc_sync
exit_from_form
в stdout.cfg, можно увидеть все изменения в состоянии формы, которые произошли после действий пользователя. Там же мы увидим строчку сгенерированную нашим классом в ответ на изменение значения Label «Updated Text for label 'label_out' (set to 10)». Значит все работает. Отлично переходим к следующему примеру
Добавим новый тест «Test_GUI_Async» в «Suite_GUI» и запишем его, как в прошлый раз, только вместо «OnCalcSync» нажмите кнопку «OnCalcAsync» и результатов расчета на этот раз придется ждать целых 10 секунд. По завершению работы нужно будет как-нибудь обозвать действие нажатия на новую кнопку, ну к примеру путь это будет «run_calc_async». Все остальные действия мы называли в прошлый раз и StoryText их запомнил.
Этот тест интересен тем, что действия на форме теперь происходят асинхронно и после нажатия на кнопку проходит десять секунд прежде чем вызывается «TestGUI._operation_finish» из модуля test.py. В этой функции, помимо расчета результата, добавлена строчка:
storytext.applicationEvent('data to be loaded')
которая говорит StoryText, что некая асинхронная операция завершила свою работу и это нужно отразить в тесте, а при автоматическом выполнении, следует остановиться в этом же месте и дождаться наступления события.
Если вы посмотрите usecase.cfg у нового теста, то увидите:
enter_data 5
run_calc_async
wait for data to be loaded
exit_from_form
т.е. отличие от предыдущего примера лишь в том, что после нажатия кнопки мы ожидаем события «data to be loaded» и только после него закрываем форму. Сохраняем результаты, запускаем еще раз, видим, что StoryText терпеливо ждет положенные 10 секунд, прежде чем завершить тест.
Как видите тестировать асинхронные события ничуть не сложно.
Все знают, что если запуск тестов требует много времени и сил, никто это делать не будет, поэтому тесты должны запускаться одной кнопкой или вообще должны прогоняться на отдельном сервере в полностью автоматическом режиме. TextTest для этого предоставляет функции пакетного выполнения с последующим формированием отчета и либо выгрузкой его в формат (html, JUnit, Jenkins), либо отсылкой по электронной почте.
Мы будем выгружать все в html. Настраивать придется только пути к папке для хранения результатов выполнения тестов и путь для хранения html отчетов. Для этого добавьте в главном config.cfg вот такие строки в конец файла:
[batch_result_repository]
default:batchresult
[historical_report_location]
default:historicalreport
Там можно указать довольно много дополнительных параметров, я на них останавливаться не буду, вот ссылка [5] на документацию по пакетному режиму.
Для автоматического запуска тестов нужно стартовать через python модуль texttest.py с параметрами "-b nightjob", примерно так:
python c:TextTesttexttest-3.24sourcebintexttest.py -b nightjob
опять же для командной строки тоже существует куча параметров, вот тут [6] можно почитать подробнее.
После выполнения, рядом с папкой simpletests появится новая batchresult с результатами, из них уже можно сформировать отчет в виде html добавив параметр "-coll web":
python c:TextTesttexttest-3.24sourcebintexttest.py -b nightjob -coll web
теперь появится еще одна папка historicalreport в которой лежит html страничка с результатами. После нескольких дней работы, она может выглядеть так:
Ну или можно посмотреть на результаты тестирования самого TextTest [7]. Так, кстати, довольно интересная статистика — около 4000 тестов запускаются ежедневно, а покрытие тестами, почти у всех модулей приближается к 100% (хотя я конечно понимаю, что сам по себе процент покрытия мало о чем говорит).
Попробуем просуммировать положительные стороны фреймворка, в основном с точки зрения GUI тестирования:
Было бы неправильно не упомянуть о минусах, тем более что они есть:
Описание было бы не полным, если не указать альтернативы для тестирования GUI на python, вот что удалось найти:
robotframework [9] сказать про него что-то определенное сложно, он очень обширен и нужно долго и вдумчиво читать документацию и пробовать. Судя по всему, это кроссплатформенный фреймворк для тестирования уровня TextTest и на него однозначно стоит посмотреть всем кто выбирает себе инструмент.
ldtp [10] следующий инструмент для тестирования GUI. Имеет три реализации
LDTP/Cobra/PyATOM для Linux/Windows/OS X соответственно. Поддерживает кучу языков (Java/Ruby/C# и т.д.) в том числе и нужный нам Python. Документация мне понравилась. Активно развивается.
Не понравился только принцип, судя по описанию тесты, пишутся как-то так:
selectmenuitem('frmUnsavedDocument1-gedit', 'mnuFile;mnuOpen')
settextvalue('frmUnsavedDocument1-gedit', 'txt0', 'Testing editing')
setcontext('Unsaved Document 1 - gedit', '*Unsaved Document 1 - gedit')
verifytoggled('dlgOpenFile...', 'tbtnTypeafilename')
т.е. ищется элемент с нужным заголовком и над ним либо совершается нужное действие или считывается состояние. Что после TextTest кажется шагом назад, но если с последним что-то все же не сложится, то ldtp будет одним из первым кандидатов на переход.
pywinauto [11] про него и на хабре можно найти пару слов [12]. Принцип такой же, как и в ldtp — находим нужный нам элемент и что-то с ним делаем. Да еще и работает он только под Windows.
Dogtail [13] судя по описанию очень сильно завязан на Unix, там даже есть отдельные версии для «GNOME 3, KDE4.8» и для «Gnome 2», так что похоже он использует для тестирования API графических оболочек, а значит заведомо не кроссплатформенен. Однако он до сих пор развивается, так что если что-то нужно потестировать под Unix, возможно стоит посмотреть в его сторону
guitest [14] библиотека для тестирования GUI у python приложений, в основном для pyGTK. Дата последнего релиза 13/11/2005. Боюсь, что проведя столько лет без развития она даже заявленные pyGTK приложения оттестировать не сможет, не говоря уж про требуемый Tkinter. Не стал даже смотреть.
pyAA [15] очередной заброшенный проект, причем заброшенный с 2005 года, завязываться на такие страшно, да и работает он только с Windows…
pyGUIUnit [16] по ссылке утверждается, что библиотечка умеет тестировать PyQt приложения. Если перейти к документации, видим, что весь фреймворк состоит из одного класса и двух функций, бегло просмотрев которые — можно понять, что ничего хорошего ждать не приходится.
Плюс существует еще довольно большое кол-во общих фреймворков тестирования, которые позволяют тестировать любое приложение через скриншоты, я выше описывал, чем они неудобны и поэтому даже не рассматривал, к тому же большинство их них работают только на Windows.
Итого: кроссплатформенных фреймворков для тестирования логики работы GUI на python с поддержкой Tkinter на удивление мало, большинство их них заброшены и забыты. Те 2-3, которые стоят того, чтобы на них посмотреть, не факт, что подойдут. TextTest пока, на мой взгляд — идеальный выбор, посмотрим, что будет дальше.
Автор: ReanGD
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/25181
Ссылки в тексте:
[1] Первая часть статьи.: http://habrahabr.ru/post/165617/
[2] Application Events: http://texttest.sourceforge.net/index.php?page=ui_testing&n=appevents
[3] отсюда: https://github.com/ReanGD/HabrArticle/blob/master/TextTest/simpletests/tkinter_ex.py
[4] тут: http://texttest.sourceforge.net/index.php?page=documentation_3_24&n=configfile_default
[5] ссылка: http://texttest.sourceforge.net/index.php?page=documentation_3_24&n=running_texttest_unattended
[6] тут: http://texttest.sourceforge.net/index.php?page=documentation_3_24&n=options_default
[7] TextTest: http://texttest.sourceforge.net/index.php?page=nightjob
[8] Хостится: https://www.reg.ru/?rlink=reflink-717
[9] robotframework: http://code.google.com/p/robotframework/
[10] ldtp: http://ldtp.freedesktop.org/wiki/
[11] pywinauto: http://pywinauto.sourceforge.net/
[12] пару слов: http://habrahabr.ru/post/138963/
[13] Dogtail: https://fedorahosted.org/dogtail/
[14] guitest: http://pypi.python.org/pypi/guitest
[15] pyAA: http://sourceforge.net/projects/uncassist/files/
[16] pyGUIUnit: http://sourceforge.net/projects/pyguiunit/
[17] Источник: http://habrahabr.ru/post/165833/
Нажмите здесь для печати.