TextTest — кроссплатформенный фреймворк на python для тестирования GUI и не только. Часть 2

  • Tutorial
Лого Продолжение рассказа о замечательном кроссплатформенном фрейворке для функционального-тестирования TextTest. Первая часть статьи.


Способы тестирования GUI


Сейчас подавляющее большинство инструментов тестирования GUI работают одним из двух способов:
  1. Делают скриншоты после каждого изменения экрана с последующим сравнением изменений
  2. Предоставляют функции, которые через API операционной системы позволяют считывать параметры виджета

Проблемы первых — неустойчивость тестов, стоит изменить взаимное расположение кнопок на форме, даже на пару пикселей, упадет куча тестов, изменится разрешение экрана — упадут все тесты, перешли на новую версию GUI-фрейморка, в которой, допустим, улучшили отрисовку шрифтов — опять же упадут все тесты. Для каждой операционной системы нужно делать свой набор тестов, возможно, все придется переделывать даже после установки очередного сервис-пака, если он затронул графическую подсистему.
Я не спорю внешний вид тестировать полезно, но это должны быть тесты только на оформление, тестировать через них логику работы GUI это издевательство над человеком.

У второго подхода другие проблемы — тесты пишутся долго и сложно, а в добавок они зачастую совершенно непонятны окружающим, особенно непрограммистам. Портянки кода вроде этого:
ActivateWindow("Unsaved Document 1 - gedit")
SetEdit(5, "5")
ClickButton("Save")
Wait(10)
VerifyLabel(2, 10)

не дают представления о том, что происходит и что собственно тестируется. Программисту нужно явно писать assert'ы, на то, что после допустим нажатия этой кнопки, вот это label изменил свое значение на такой, а вот этот — не изменил. Так же API операционной системы может не предоставлять полного доступа к внутренностям виджетов, а уж если это нестандартный виджет, то считать его свойства становится практически невозможно.

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

И наконец, у обоих подходов есть общая проблема с тестированием не синхронных событий. Допустим, ввели в браузер URL, а когда проверять что страница загрузилась?

Можно делать таймаут, но тут проблема, что если страница в среднем загружается за 5 сек, то нужно «для верности» выставить таймаут раз в 5 больше, да и то не факт, что все отработает. Это приводит к замедлению тестов и к вероятностному характеру их выполнения.

Вариант второй — «заточится» на некоторые тайные виджеты и как только они примут нужное состояние, считать, что действие выполнилось. Это добавляет работы программисту и делает тесты еще более нечитабельными и непонятными.

Какие же альтернативы предлагает TextTest?


Он для решения описанных выше проблем предлагает использовать библиотеку StoryText. Последняя перед запуском вашего приложения «оборачивает» интерфейсы обращения к библиотеке GUI, подсовывая программе свои варианты интерфейсов. Причем делается это совершенно прозрачно. В большинстве случаев, вам не придется менять ни единой строчки кода в тестируемом приложении. Это дает возможность при записи теста сохранять в лог действия пользователя и реакцию программы на эти действия. А при тестировании — повторять действия пользователя и сравнивать реакцию программы с эталонной.

Итак, что же это нам дает:
  • Мы абстрагируемся от расположения элементов на форме и их внешнего вида, нам абсолютно не важно, что в разных OS кнопки имеют разный вид, мы тестируем только логику. Если нам нужно проверить, что после нажатия на кнопке изменилось значения label, то никакие дизайнерские изменения формы этот тест нам не поломают.

  • Получаем полный доступ к виджету, никакие WinApi не позволят получить столько информации о виджете, как нативная библиотека создающая этот виджет.

  • Для асинхронных событий вводится такая понятие как Application Events которое, хоть и заставляет немного видоизменить код, но это всего лишь одна строчка на каждое асинхронное событие. Если кратко, то суть последних в том, что по завершению асинхронного события мы вызываем код, уведомляющий StoryText, что операция завершилась. StoryText запоминает этот факт и при воспроизведении будет честно ждать этого события от приложения и только потом продолжит тест. Все! Никаких таймаутов, никаких скрытых виджетов и прочих извращений, все просто и максимально быстро

  • Легкость написания тестов, позволяет их создавать даже слабо знакомому с программированием человеку. Не приходится кодом искать нужную кнопку и кликать на нее, не нужно потом ассертить значения изменившихся элементов, StoryText сам записывает все события, которые выполнил пользователь и сам сохраняет все изменения в GUI, причем, как вы убедитесь в дальнейшем, делает это в довольно человеко-читаемом виде.

  • Легкость понимания тестов. После записи первого теста StoryText предлагает ввести алиасы для всех произошедших событий, т.е. автоматически созданный тест будет выглядеть не так:
    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, остальные добавлены в неком экспериментальном режиме и реализованы не полностью, однако ниже я покажу, насколько не сложно добавлять новую функциональность.

Четвертый пример. Тестируем синхронное GUI на Tkinter


Тестировать будет класс TestGUI. Класс создает очень простую форму на которой предлагается ввести значение, и нажать одну из кнопок OnCalcSync или OnCalcAsync, первая выведет значение, умноженное на два сразу, вторая подождет 10 секунд и выведет тоже самое. Да и я заранее извиняюсь за ужасный вид формы, но это же только пример.
класс TestGUI
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 (скачать можно отсюда и положить рядом с 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 код получился не сложный
tkinter_ex.py
# -*- 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». в simpletests\config.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

Расшифровывать все настройки не буду, про них можно почитать вот тут. Обращу внимание только на одну: «virtual_display_count», она должна иметь смысл только на UNIX системах и позволяет запускать тесты на виртуальных дисплеях через Xvfb. Но из-за ошибки реализации если этот параметр не выставить, StoryText пытается создавать виртуальные дисплеи на Windows, где Xvfb отсутствует. Поэтому настройку нужно явно добавлять и выставлять в 0.

Не забываем перезагрузить IDE после внесения настроек. После чего в меню «Test_GUI_Sync» станет доступен пункт «Record Use-Case», запускаем, ничего в появившемся окне не меняем. Появляется наша тестовая неказистая форма:
Форма GUI

в ней нужно выполнить действия, которые хотим оттестировать, вводим значение в поле «Input», нажимаем «OnCalcSync» и потом «Exit». После этого StoryText предложит нам дать человеческие имена для выполненных действий, заполним их, например вот так:
Настройка действий

Как обычно сохраняем и пробуем запустить тест еще раз, все отлично отрабатывает.
Чтобы понять что произошло можно заглянуть в появившийся на диске: simpletests\storytext_files\ui_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)». Значит все работает. Отлично переходим к следующему примеру

Пятый пример. Тестируем асинхронное GUI на Tkinter


Добавим новый тест «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

Там можно указать довольно много дополнительных параметров, я на них останавливаться не буду, вот ссылка на документацию по пакетному режиму.
Для автоматического запуска тестов нужно стартовать через python модуль texttest.py с параметрами "-b nightjob", примерно так:
python c:\TextTest\texttest-3.24\source\bin\texttest.py -b nightjob

опять же для командной строки тоже существует куча параметров, вот тут можно почитать подробнее.
После выполнения, рядом с папкой simpletests появится новая batchresult с результатами, из них уже можно сформировать отчет в виде html добавив параметр "-coll web":
python c:\TextTest\texttest-3.24\source\bin\texttest.py -b nightjob -coll web

теперь появится еще одна папка historicalreport в которой лежит html страничка с результатами. После нескольких дней работы, она может выглядеть так:
Результаты тестирования

Ну или можно посмотреть на результаты тестирования самого TextTest. Так, кстати, довольно интересная статистика — около 4000 тестов запускаются ежедневно, а покрытие тестами, почти у всех модулей приближается к 100% (хотя я конечно понимаю, что сам по себе процент покрытия мало о чем говорит).

Подведем итоги


Плюсы

Попробуем просуммировать положительные стороны фреймворка, в основном с точки зрения GUI тестирования:
  • Кроссплатформенность, кроссбиблиотечность (имеются в виду GUI библиотеки PyGTK, Tkinter и т.п.), частично даже присутствует кроссязыковая поддержка (автор часто упоминает работу с Java, правда не знаю насколько это удобно)

  • Бесплатность и открытость исходников.

  • Активность автора в доработке фреймворка, что для 10-и летнего проекта удивительно.

  • После первоначальной настройки — простота использования.

  • Проведено много работы, чтобы дать возможность использовать продукт непрограммистами, а сами тесты сделать человеко-читаемыми.

  • Для тестирования GUI используются не API операционных систем, а непосредственно API инструментария (PyGTK, Tkinter и т.п.), что делает интеграцию максимально полной и в то же время простой.

  • Позволяет тестировать именно логику работы, абстрагируясь от внешнего вида.

  • Довольно гибок, столкнувшись с проблемой, почти всегда можно полазить по документации и отыскать как все поправить настройкой.

  • Позволяет интегрироваться с популярными багтрекерами (Bugzilla, Jira, Trac), системами контроля версий (CVS, Bazaar, Mercurial), а результаты работы выгружать в форматах (html, JUnit, Jenkins) или отправлять по почте. Причем добавить интеграцию с другими системами, похоже не особо сложно. Хотя признаюсь, я еще не пробовал насколько хорошо он умеет работать со всеми этими системами.


Минусы

Было бы неправильно не упомянуть о минусах, тем более что они есть:
  • Первое это GUI, оно довольно мало функционально, настройка параметров в основном производится не через него, а через документацию + ручную правку конфигов. Справедливости ради нужно сказать, что настройка выполняется один раз, а потом только клепаем типовые тесты. Но на первоначальном этапе изучения очень не хватает возможности быстро расставить галочки и начать работать, чтобы понять что к чему.

  • Стабильность GUI, иногда оно падает, даже не знаю к чему это отнести, то ли к работе PyGTK, то ли к слабой оттестированности под Windows, но факт остается фактом. Но опять же GUI используется только при создании теста, а прогоняется потом множество раз и как раз с последним проблем я не наблюдал.

  • Иногда встречаются ошибки и приходится лезть в код, чтобы понять что произошло (я выше приводил пример с параметром virtual_display_count.) Однако все с чем я сталкивался тем или иным образом решается, тем более исходники открыты и всегда можно посмотреть, подправить что нужно. Тут опять же я грешу на Windows, похоже автора больше интересует работа под UNIX-системами, там надеюсь, проблем таких нет.

  • Документация. Может дело во мне, но документацию приходится читать очень и очень вдумчиво, по нескольку раз, чтобы понять, как работает очередная фишка. Некоторые понятные вещи расписаны очень хорошо, а в сложных — упущены нюансы. Имеющуюся документацию довольно удобно использовать как справочник, но только после того как понимаешь, как работает та или иная вещь.

  • Комьюнити. Тут дела совсем плохо, в русском интернете вообще про фреймворк все молчат, максимум упоминают, что он существует. В английском, все, что я находил, были рассказы об идеологии и про то, насколько он хорош. Каких-то толковых описаний, мануалов, статей и т.п. я не нашел, все только на сайте автора. Этот факт я объяснить никак не могу, фреймворк выглядит очень зрелым, интересным и «вкусным», альтернатив фактически нет, что с ним не так, не понимаю.

  • Хостится он на launchpad, а не на популярном нынче GitHub, боюсь, что этот факт оттолкнет многих, кто имеет желание и возможности помочь автору в написании кода.


Альтернативы

Описание было бы не полным, если не указать альтернативы для тестирования GUI на python, вот что удалось найти:

robotframework сказать про него что-то определенное сложно, он очень обширен и нужно долго и вдумчиво читать документацию и пробовать. Судя по всему, это кроссплатформенный фреймворк для тестирования уровня TextTest и на него однозначно стоит посмотреть всем кто выбирает себе инструмент.

ldtp следующий инструмент для тестирования 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 про него и на хабре можно найти пару слов. Принцип такой же, как и в ldtp — находим нужный нам элемент и что-то с ним делаем. Да еще и работает он только под Windows.

Dogtail судя по описанию очень сильно завязан на Unix, там даже есть отдельные версии для «GNOME 3, KDE4.8» и для «Gnome 2», так что похоже он использует для тестирования API графических оболочек, а значит заведомо не кроссплатформенен. Однако он до сих пор развивается, так что если что-то нужно потестировать под Unix, возможно стоит посмотреть в его сторону

guitest библиотека для тестирования GUI у python приложений, в основном для pyGTK. Дата последнего релиза 13/11/2005. Боюсь, что проведя столько лет без развития она даже заявленные pyGTK приложения оттестировать не сможет, не говоря уж про требуемый Tkinter. Не стал даже смотреть.

pyAA очередной заброшенный проект, причем заброшенный с 2005 года, завязываться на такие страшно, да и работает он только с Windows…

pyGUIUnit по ссылке утверждается, что библиотечка умеет тестировать PyQt приложения. Если перейти к документации, видим, что весь фреймворк состоит из одного класса и двух функций, бегло просмотрев которые — можно понять, что ничего хорошего ждать не приходится.

Плюс существует еще довольно большое кол-во общих фреймворков тестирования, которые позволяют тестировать любое приложение через скриншоты, я выше описывал, чем они неудобны и поэтому даже не рассматривал, к тому же большинство их них работают только на Windows.

Итого: кроссплатформенных фреймворков для тестирования логики работы GUI на python с поддержкой Tkinter на удивление мало, большинство их них заброшены и забыты. Те 2-3, которые стоят того, чтобы на них посмотреть, не факт, что подойдут. TextTest пока, на мой взгляд — идеальный выбор, посмотрим, что будет дальше.
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 2
  • 0
    Я правильно понял, что TextTest является своеобразным GUI прокси — самостоятельно отрисовывает тестовое окно и записывает все действия?
    А есть ли возможность тестировать UI уже готового exe?
    • 0
      Нет, StoryText не отрисовывает ничего самостоятельно.

      Посмотрите на tkinter_ex.py, там я унаследовался от Tkinter.Label переопределил некоторые функции, добавил в них логирование и вызывал исходные функции Tkinter.Label, т.е. функциональность осталась прежняя + логирование. Потом я присвоил мой класс в Tkinter.Label обратно.

      Теперь программа сама того не подозревая, вызывает мои функции с логированием. StoryText сделан на тех же принципах.

      И реализовано это только для python. Готовые не python программы она таким образом не умеет обрабатывать.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.