SWAPY – графическая утилита для автоматизации UI для pywinauto (Python).
В версии 0.4.7 полностью переработан генератор кода.
Основные возможности, а также примеры как быстро и просто создать скрипты автоматического тестирования UI, смотрите под катом.
Описание
SWAPY – графическая утилита для просмотра иерархии окон и генерации кода автоматизации UI для библиотеки pywinauto.
Само название это акроним, отражающий основную идею приложения — Simple Windows Automation on PYthon. Утилита представляет собой полноценный exe файл, собранный с помощью PyInstaller. SWAPY не требует никаких дополнительных установок для автоматизации и генерации кода. Конечно, для дальнейшего использования кода вам понадобится установить как минимум Python и pywinauto. Но для проверки возможностей и, самое главное, подойдет ли такая связка для автоматизации Вашего приложения, SWAPY вполне самодостаточна.
Утилита содержит три основных компонента, это:
- дерево объектов
- таблица свойств выбранного объекта
- поле с кодом
Чтобы создать скрипт, необходимо найти элемент в дереве всех контролов и затем вызвать действие, например, Click. При этом выполнится как само действие над объектом, так и обновится поле с кодом.
Раньше генератору кода уделялось мало внимания. Чаще использовались функции поиска элемента и просмотра его параметров. Все фиксы и фичи для генератора кода добавлялись по остаточному принципу. В итоге, чтобы получить рабочий код, нужны были определенные усилия со стороны пользователя — необходимо было последовательно проинициализировать всех предков.
Новый генератор кода, в основном, лишен прежних недостатков.
История развития
В начале 2011 года, будучи на должности «Automation QA Engineer», открыл для себя библиотеку для автоматизации UI – pywinauto. Об истории развития самой библиотеки можно кое-что узнать в статье «Старый новый pywinauto». На тот момент она практически не поддерживалась. Тем не менее Pywinauto победила всех своих конкурентов и была выбрана для тестирования ряда продуктов со средней сложностью графического интерфейса.
Отмечу основные преимущества, благодаря которым выбор пал именно на этот вариант:
- Цена инструмента. Pywinauto бесплатна, распространяется под лицензией GNU LGPL v.2.1
- Это библиотека Python. Со всеми его возможностями, библиотеками, и т.д.
- Простая подготовка окружения. Подготовить виртуальную машину для тестирования установив Python + pywinauto сильно проще установки, например, такого монстра как TestComplete. Это весьма актуально в контексте использования Continuous Integration.
Вскоре обнаружился один недостаток — тратится много времени на поиск необходимого элемента и анализ его свойств. Очень не хватало графической утилиты для просмотра дерева элементов и их параметров. Библиотеке для автоматизации графических интерфейсов было бы не плохо иметь графический интерфейс.
Было решено исправить эту несправедливость.
В апреле 2011 года я начал работу над утилитой, к концу года версия стремительно выросла до 0.3.0, а утилита уже имела все ключевые составляющие и… множество проблем…
В течение следующего года потихоньку исправлялись ошибки и что-то незначительное добавлялось. А потом я сменил работу и интерес поддерживать утилиту, которую сам не использую, да еще и в одиночку, пропал.
Второе дыхание SWAPY получил в сентябре 2015, когда ребята из pywinauto позвали к себе.
С тех пор стал снова активно развивать утилиту. Ключевым улучшением можно назвать новый генератор кода.
Я пересмотрел свое отношение к функции генерации кода как к одной из основных функций. Именно через генератор кода можно познакомить разработчика с дополнительными возможностями библиотеки, а также спасти даже опытного разработчика от рутины.
Новые возможности
- Генератор кода теперь работает нормально. Имеется ввиду что не нужно кликать по всем предкам в дереве объекта, чтобы получился рабочий код. Сейчас достаточно отыскать необходимый элемент и выполнить над ним действие, код будет автоматически построен вплоть до импорта. Один клик в новой версии:
from pywinauto.application import Application app = Application().Start(cmd_line=u'"C:\\Program Files (x86)\\Notepad++\\notepad++.exe" ') notepad = app[u'Notepad++'] notepad.Wait('ready') systabcontrol = notepad.Tab systabcontrol.Select(u'new 1') app.Kill_()
В старой версии аналогичное действие приводит к такому результату:
import pywinauto pwa_app = pywinauto.application.Application() ctrl.Select(0)
Очевидно что такой код работать не будет.
- Понятные имена переменных. Согласитесь,
systabcontrol
намного понятнее какого тоctrl
. Имена формируются на базе имени класса контрола, либо из самого короткого имени для доступа (из pywinauto). Только если оба эти случая были безуспешными, то будет использовано безликое —control
. - Контроль над одинаковыми именами переменных. Если необходимо работать с разными контролами, имеющими одинаковые имена, SWAPY следит что бы они оставались уникальными.
button = calcframe.Button19 button.Click() button2 = calcframe.Button20 button2.Click()
Это актуально для следующего пункта.
- Повторное использование. Как правило, действие состоит из двух строк. В первой происходит инициализация доступа к контролу, во второй — непосредственно действие. Так вот, если понадобилось в какой то момент повторить действие над контролом, который уже был инициализирован, то добавляется просто код действия.
button = calcframe.Button19 button.Click() button2 = calcframe.Button20 button2.Click() button.Click() # Повторный Click по Button19
Отмена последней команды. Частенько возникает необходимость удалить последнюю команду, например после неудачных экспериментов. Теперь есть возможность сделать это через контекстное меню редактора. При этом имя исчезнувшей переменной освободится и будет использовано в следующий раз. Отменять можно любое количество шагов. Нужно понимать, что отмена последней команды лишь очистит код, действие в приложении не будет отменено.
Также есть возможность очистить сразу весь код, а еще можно сохранить весь код в файл.
Изменение кода «на лету». Пока эта функциональность используется в окнах верхнего уровня для переключения междуapp = Application().Start(cmd_line=...
иapp = Application().Connect(title=...
. В большинстве случаев будет достаточноStart
, но если не нужно запускать приложение, то следует выбратьApplication.Connect
в контекстном меню дерева объектов, кликнув на имени окна. Код в редакторе обновится, исчезнут привязанные к методуApplication().Start
команды —calcframe.Wait('ready')
в начале иapp.Kill_()
в конце.
Пример кода со стартом приложения.
from pywinauto.application import Application app = Application().Start(cmd_line=u'"C:\\Program Files (x86)\\Notepad++\\notepad++.exe" ') notepad = app[u'Notepad++'] notepad.Wait('ready') app.Kill_()
Подключается к уже запущенному приложению.
from pywinauto.application import Application app = Application().Connect(title=u'new 1 - Notepad++', class_name='Notepad++') notepad = app[u'Notepad++']
Пример использования
Теперь давайте создадим несколько скриптов для автоматизации тестирования. Я постарался выбрать достаточно жизненные примеры и одновременно продемонстрировать новые фичи кодогенератора.
Текст лицензии
В этом тесте мы проверим что текст лицензии отображается на диалоге About. Одновременно убедимся что SWAPY понимает, что новое окно принадлежит старому приложению и не будет создавать лишних вызовов app = Application().Start(...)
- Запускаем вручную Notepad++.
- Находим в дереве элементов SWAPY нужный элемент меню и кликаем на него.
- Чтобы обновить дерево элементов для отображения вновь открытого окна, нужно поставить выделение на
root
элемент в дереве. При этом все дочерние элементы обновятся. - Находим About диалог, он у меня называется
Window#657198
, это SWAPY сама сформировала название из handle окна, так как обычным способом(window.Texts()
) имя не определилось. - В иерархии About диалога находим текст лицензии и кликаем на него.
Добавились только следующие строчки:
window = app.Dialog edit = window.Edit2 edit.Click() # Изменим на получение текста
Т.е. SWAPY использовала существующую переменную
app
.
С автогенерацией кода для этого теста мы закончили. Обратите внимание что Notepad++ будет запущен и закрыт после теста, за это отвечает последняя строкаapp.Kill_()
.
Финальный код теста может выглядеть следующим образом:
from pywinauto.application import Application expected_text = “...” app = Application().Start(cmd_line=u'"C:\\Program Files (x86)\\Notepad++\\notepad++.exe" ') notepad = app[u'Notepad++'] notepad.Wait('ready') menu_item = notepad.MenuItem(u'&?->\u041e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0435...\tF1') menu_item.Click() window = app.Dialog edit = window.Edit2 actual_text = edit.Texts() app.Kill_() assertEqual(expected_text, actual_text)
Как видите, минимум собственного кода.
Порядок вкладок
Давайте проверим перемещение вкладок. Нарочно допустим ошибку при генерации кода и посмотрим как SWAPY позволит её убрать.
- Запускаем вручную Notepad++.
- Откроем две дополнительные вкладки. Находим в дереве элементов необходимый
ToolBar
и выполняем действиеClick
на кнопке с индексом 0. Вследствие чего появится код и откроется одна новая вкладка.
Нам нужна еще одна вкладка, повторим действие еще раз. Поскольку текст кнопок недоступен, используется адресация по индексу. Мы не заметили и нечаянно кликнули на кнопку с индексом 1.
Добавился код:
toolbar_button2 = toolbarwindow.Button(1) toolbar_button2.Click()
Нужно исправляться. Чтобы не повторять все сначала, SWAPY позволяет отменить последнюю команду (можно последовательно отменить хоть весь код).
Clear last command
отменит последнюю команду (выделенный фрагмент) — как раз то, что нам и нужно. Чтобы полностью очистить код, есть командаClear the code
. Полная очистка спрятана за диалогом с подтверждением, во избежание несчастных случаев на производстве.
Теперь мы сделаем все правильно и кликнем по кнопке с индексом 0.
Добавится код:toolbar_button.Click()
SWAPY помнит что уже есть
toolbar_button = toolbarwindow.Button(0)
и для повторного клика инициализировать его уже не нужно. - Для drug-n-drop воспользуемся методом
toolbarwindow.DragMouseInput
. Детали использования можно подсмотреть в документации.
Координаты вкладок можно определить с помощьюsystabcontrol.GetTabRect(0).mid_point()
Тест может выглядеть так:
# automatically generated by SWAPY from pywinauto.application import Application app = Application().Start(cmd_line=u'"C:\\Program Files (x86)\\' u'Notepad++\\notepad++.exe" ') notepad = app[u'Notepad++'] notepad.Wait('ready') systabcontrol = notepad.Tab assertEqual([u'Tab', u'new 1'], systabcontrol.Texts()) toolbarwindow = notepad[u'3'] toolbar_button = toolbarwindow.Button(0) toolbar_button.Click() toolbar_button.Click() assertEqual([u'Tab', u'new 1', u'new 2', u'new 3'], systabcontrol.Texts()) systabcontrol.DragMouseInput( press_coords=systabcontrol.GetTabRect(0).mid_point(), release_coords=systabcontrol.GetTabRect(2).mid_point()) assertEqual([u'Tab', u'new 2', u'new 3', u'new 1'], systabcontrol.Texts()) app.Kill_()
Тут пришлось немного почитать документацию и немного поработать с генерированным кодом.
Вставка и сохранение текста
Тест требует проверить копирование и вставку текста с последующим сохранением. Усложним задачу — Notepad++ уже запущен и свернут (Minimize), а стандартный notepad (из которого будет производится копирование) только предстоит запустить.
- Подготовим тестовые приложения. Запустим и свернем Notepad++, запустим обычный notepad с тестовым файлом = «notepad check.txt».
- В дереве объектов найдем блокнот и кликнем по содержимому редактора.
Обратите внимание, что notepad будет запущен с оригинальными аргументами. - Теперь отыщем Notepad++ и его текстовое поле. Нужно не забыть его сначала развернуть (Restore).
Все идет по плану, но тут внезапно мы вспомнили, что по условию задачи Notepad++ уже запущен, а наш код попытается его запустить.
SWAPY по умолчанию генерирует связкуapp = Application().Start ... app.Kill_()
. Но в нашем случае нам не нужно еще раз запускать Notepad++.
Новый генератор кода позволяет изменять «подход» для генерации кода, причем это можно делать даже постфактум. - Для изменения
Application().Start
наApplication().Connect
нужно вызвать контекстное меню для окна приложения Notepad++ и выбратьApplication().Connect
.
- Копирование и вставку текста мы оформим позже, а сейчас предположим что текст есть и его нужно сохранить.
- Открылось окно «Save as», необходимо обновить дерево элементов что бы его увидеть. Для этого нужно выделить root элемент дерева. После обновления дерева, кликнем на поле с именем сохраняемого файла (чтобы потом поменять) и на кнопку для сохранения.
Все основные действия есть, теперь осталось добавить отправку команд CTRL+C, CTRL+V и проверки чтобы получился настоящий тест.
Для отправки команд, воспользуемся встроенным методом TypeKeys.
Полный текст приведен ниже:
# automatically generated by SWAPY from pywinauto.application import Application import time import os SAVE_PATH = r"Notepad_default_path" app = Application().Start(cmd_line=u'"C:\\Windows\\system32\\notepad.exe" check.txt') notepad = app.Notepad notepad.Wait('ready') edit = notepad.Edit edit.TypeKeys("^a^c") # Copy all the text app2 = Application().Connect(title=u'new 1 - Notepad++', class_name='Notepad++') notepad2 = app2[u'Notepad++'] notepad2.Restore() scintilla = notepad2[u'1'] scintilla.TypeKeys("^a^v") # Paste the text #Save a file menu_item = notepad2.MenuItem(u'&\u0424\u0430\u0439\u043b->\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043a\u0430\u043a...\tCtrl+Alt+S') menu_item.Click() window = app2.Dialog edit2 = window.Edit filename = "checked_at_%s" % time.time() # Compose a filename edit2.TypeKeys(filename) button = window.Button button.Click() with open(os.path.join(SAVE_PATH, filename)) as f: assertEqual(“expected_text”, f.read()) app.Kill_()
А можно еще лучше?
Безусловно — Да!
Даже в описанных примерах мы вынуждены были делать Click()
а потом уже вручную менять на получение текста — Texts()
. Или же вручную добавляли TypeKeys
. В будущих релизах еще предстоит упростить такие популярные действия, добавив дополнительные пункты в контекстное меню.
Пока нельзя управлять форматом доступа к элементам. Pywinauto позволяет получить доступ к элементам через атрибуты — window.Edit
, а если это невозможно (недопустимое имя для переменной Python), то через __getitem__
— window[u'0']
.
SWAPY находит самое короткое имя для доступа и пробует его применить в качестве атрибута. Если не получается, то через __getitem__
. Идея пока самая простая — получить короткий код.
Но, например, в тесте «Порядок вкладок» есть такая строчка toolbarwindow = notepad[u'3']
. Все работает, все ОК. Но, представьте, вы открыли этот тест через некоторое время, а там такой magic number. Вместо тройки могло бы быть Toolbar
— самое понятное, а не самое короткое имя. В планах — дать юзеру возможность выбирать имя (“Имя! Имя, сестра!”).
Также пока нужно обновлять дерево объектов вручную. Автоматический refresh явно добавит удобства.
Полезные ссылки
- pywinauto
- SWAPY
- Старый новый pywinauto
- Динамическое (нелинейное) тестирование GUI
- Win32 GUI Automation при помощи pywinauto
P.S.
Хотел бы поблагодарить камрадов vasily-v-ryabov и airelil за активное участие в обсуждении фич для нового генератора кода.
ссылка на оригинал статьи http://habrahabr.ru/post/270247/
Добавить комментарий