SWAPY с новым генератором кода
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 явно добавит удобства.
Полезные ссылки
P.S.
Хотел бы поблагодарить камрадов vasily-v-ryabov и airelil за активное участие в обсуждении фич для нового генератора кода.