Автоматизируем десктопный GUI на Python + pywinauto: как подружиться c MS UI Automation |
24.10.2017 11:12 |
Автор: Василий Рябов (SDET, Aquantia Corp.). Оригинальная публикация: https://habrahabr.ru/post/323962/ Python библиотека pywinauto — это open source проект по автоматизации десктопных GUI приложений на Windows. За последние два года в ней появились новые крупные фичи:
Также сделаем небольшой обзор того, что есть в open source для десктопной автоматизации (без претензий на серьезное сравнение). Эта статья — частично расшифровка доклада с конференции SQA Days 20 в Минске (видеозапись и слайды), частично русская версия Getting Started Guide для pywinauto.
Начнём с краткого обзора опен сорса в этой области. Для десктопных GUI приложений всё несколько сложнее, чем для веба, у которого есть Selenium. Вот основные подходы: Координатный метод Хардкодим точки кликов, надеемся на удачные попадания. [-] Автоматизирует только действия, для верификации и извлечения данных есть другие методы. Инструменты (кросс-платформенные): autopy, PyAutoGUI, PyUserInput и многие другие. Как правило, более сложные инструменты включают в себя эту функциональность (не всегда кросс-платформенно). Стоит сказать, что координатный метод может дополнять остальные подходы. Например, для кастомной графики можно кликать по относительным координатам (от левого верхнего угла окна/элемента, а не всего экрана) — обычно это достаточно надежно, особенно если учитывать длину/ширину всего элемента (тогда и разное разрешение экрана не помешает). Другой вариант: выделять для тестов только одну машину со стабильными настройками (не кросс-платформенно, но в каких-то случаях годится). Распознавание эталонных изображений [+] Кросс-платформенный Инструменты: Sikuli, Lackey (Sikuli-совместимый, на чистом Python), PyAutoGUI. Accessibility технологии [+] Самый надежный метод, т.к. позволяет искать по тексту, независимо от того, как он отрисован системой или фреймворком. Инструменты: TestStack.White на C#, Winium.Desktop на C# (Selenium совместимый), MS WinAppDriver на C# (Appium совместимый), pywinauto, pyatom (совместим с LDTP), Python-UIAutomation-for-Windows, RAutomation на Ruby, LDTP (Linux Desktop Testing Project) и его Windows версия Cobra. LDTP — пожалуй, единственный кросс-платформенный open-source инструмент (точнее семейство библиотек) на основе accessibility технологий. Однако он не слишком популярен. Сам не пользовался им, но по отзывам интерфейс у него не самый удобный. Если есть позитивные отзывы, прошу поделиться в комментах. Тестовый backdoor (a.k.a. внутренний велосипед) Для кросс-платформенных приложений сами разработчики часто делают внутренний механизм для обеспечения testability. Например, создают служебный TCP сервер в приложении, тесты к нему подключаются и посылают текстовые команды: на что нажать, откуда взять данные и т.п. Надежно, но не универсально. Основные десктопные accessibility технологии Старый добрый Win32 API Большинство Windows приложений, написанных до выхода WPF и затем Windows Store, построены так или иначе на Win32 API. А именно, MFC, WTL, C++ Builder, Delphi, VB6 — все эти инструменты используют Win32 API. Даже Windows Forms — в значительной степени Win32 API совместимые. Инструменты: AutoIt (похож на VB) и Python обертка pyautoit, AutoHotkey (собственный язык, есть IDispatch COM интерфейс), pywinauto (Python), RAutomation (Ruby), win32-autogui (Ruby). Microsoft UI Automation Главный плюс: технология MS UI Automation поддерживает подавляющее большинство GUI приложений на Windows за редкими исключениями. Проблема: она не сильно легче в изучении, чем Win32 API. Иначе никто бы не делал оберток над ней. Фактически это набор custom COM интерфейсов (в основном, UIAutomationCore.dll), а также имеет .NET оболочку в виде namespace System.Windows.Automation. Она, кстати, имеет привнесенный баг, из-за которого некоторые UI элементы могут быть пропущены. Поэтому лучше использовать UIAutomationCore.dll напрямую (если слышали про UiaComWrapper на C#, то это оно). Разновидности COM интерфейсов: (1) Базовый IUknown — "the root of all evil". Самый низкоуровневый, ни разу не user-friendly. Инструменты: TestStack.White на C#, pywinauto 0.6.0+, Winium.Desktop на C#, Python-UIAutomation-for-Windows (у них исходный код сишных оберток над UIAutomationCore.dll не раскрыт), RAutomation на Ruby. AT-SPI Несмотря на то, что почти все оси семейства Linux построены на X Window System (в Fedora 25 "иксы" поменяли на Wayland), "иксы" позволяют оперировать только окнами верхнего уровня и мышью/клавиатурой. Для детального разбора по кнопкам, лист боксам и так далее — существует технология AT-SPI. У самых популярных оконных менеджеров есть так называемый AT-SPI registry демон, который и обеспечивает для приложений автоматизируемый GUI (как минимум поддерживаются Qt и GTK). Инструменты: pyatspi2. pyatspi2, на мой взгляд, содержит слишком много зависимостей типа того же PyGObject. Сама технология доступна в виде обычной динамической библиотеки libatspi.so. К ней имеется Reference Manual. Для библиотеки pywinauto планируем реализовать поддержку AT-SPI имеено так: через загрузку libatspi.so и модуль ctypes. Есть небольшая проблема только в использовании нужной версии, ведь для GTK+ и Qt приложений они немного разные. Вероятный выпуск pywinauto 0.7.0 с полноценной поддержкой Linux можно ожидать в первой половине 2018-го. Apple Accessibility API На MacOS есть собственный язык автоматизации AppleScript. Для реализации чего-то подобного на Python, разумеется, нужно использовать функции из ObjectiveC. Начиная, кажется, еще с MacOS 10.6 в предустановленный питон включается пакет pyobjc. Это также облегчит список зависимостей для будущей поддержки в pywinauto. Инструменты: Кроме языка Apple Script, стоит обратить внимание на ATOMac, он же pyatom. Он совместим по интерфейсу с LDTP, но также является самостоятельной библиотекой. На нем есть пример автоматизации iTunes на macOs, написанный моим студентом. Есть известная проблема: не работают гибкие тайминги (методы waitFor*). Но, в целом, неплохая вещь. Как начать работать с pywinauto Первым делом стоит вооружиться инспектором GUI объектов (то, что называют Spy tool). Он поможет изучить приложение изнутри: как устроена иерархия элементов, какие свойства доступны. Самые известные инспекторы объектов:
Просветив приложение насквозь, выбираем бэкенд, который будем использовать. Достаточно указать имя бэкенда при создании объекта Application.
Входные точки для автоматизации Приложение достаточно изучено. Пора создать объект Application и запустить его или присоединиться к уже запущенному. Это не просто клон стандартного класса subprocess.Popen, а именно вводный объект, который ограничивает все ваши действия границами процесса. Это очень полезно, если запущено несколько экземпляров приложения, а остальные трогать не хочется. from pywinauto.application import Application
app = Application(backend="uia").start('notepad.exe')
# Опишем окно, которое хотим найти в процессе Notepad.exe
dlg_spec = app.UntitledNotepad
# ждем пока окно реально появится
actionable_dlg = dlg_spec.wait('visible') Если хочется управлять сразу несколькими приложениями, вам поможет класс Desktop. Например, в калькуляторе на Win10 иерархия элементов размазана аж по нескольким процессам (не только calc.exe). Так что без объекта Desktop не обойтись. from subprocess import Popen
from pywinauto import Desktop
Popen('calc.exe', shell=True)
dlg = Desktop(backend="uia").Calculator
dlg.wait('visible') Корневой объект (Application или Desktop) — это единственное место, где нужно указывать бэкенд. Все остальное прозрачно ложится в концепцию "спецификация->враппер", о которой дальше. Спецификации окон/элементов Это основная концепция, на которой строится интерфейс pywinauto. Вы можете описать окно/элемент приближенно или более детально, даже если оно еще не существует или уже закрыто. Спецификация окна (объект WindowSpecification) хранит в себе критерии, по которым нужно искать реальное окно или элемент. Пример детальной спецификации окна: >>> dlg_spec = app.window(title='Untitled - Notepad') >>> dlg_spec
<pywinauto.application.WindowSpecification object at 0x0568B790> >>> dlg_spec.wrapper_object()
<pywinauto.controls.win32_controls.DialogWrapper object at 0x05639B70> Сам поиск окна происходит по вызову метода .wrapper_object(). Он возвращает некий "враппер" для реального окна/элемента или кидает ElementNotFoundError (иногда ElementAmbiguousError, если найдено несколько элементов, то есть требуется уточнить критерий поиска). Этот "враппер" уже умеет делать какие-то действия с элементом или получать данные из него. Python может скрывать вызов .wrapper_object(), так что финальный код становится короче. Рекомендуем использовать его только для отладки. Следующие две строки делают абсолютно одно и то же: dlg_spec.wrapper_object().minimize() # debugging
dlg_spec.minimize() # production Есть множество критериев поиска для спецификации окна. Вот лишь несколько примеров: # могут иметь несколько уровней
app.window(title_re='.* - Notepad$').window(class_name='Edit') # можно комбинировать критерии (как AND) и не ограничиваться одним процессом приложения
dlg = Desktop(backend="uia").Calculator
dlg.window(auto_id='num8Button', control_type='Button') Список всех возможных критериев есть в доках функции pywinauto.findwindows.find_elements(...). Магия доступа по атрибуту и по ключу Python упрощает создание спецификаций окна и распознает атрибуты объекта динамически (внутри переопределен метод __getattribute__). Разумеется, на имя атрибута накладываются такие же ограничения, как и на имя любой переменной (нельзя вставлять пробелы, запятые и прочие спецсимволы). К счастью, pywinauto использует так называемый "best match" алгоритм поиска, который устойчив к опечаткам и небольшим вариациям. app.UntitledNotepad
# то же самое, что
app.window(best_match='UntitledNotepad') Если все-таки нужны Unicode строки (например, для русского языка), пробелы и т.п., можно делать доступ по ключу (как будто это обычный словарь): app['Untitled - Notepad']
# то же самое, что
app.window(best_match='Untitled - Notepad') Пять правил для магических имен Как узнать эталонные магические имена? Те, которые присваиваются элементу перед поиском. Если вы указали имя, достаточно похожее на эталон, значит элемент будет найден.
Обычно два-три правила применяются одновременно, редко больше. Чтобы проверить, какие конкретно имена доступны для каждого элемента, можно использовать метод print_control_identifiers(). Он может печатать дерево элементов как на экран, так и в файл. Для каждого элемента печатаются его эталонные магические имена. Также можно скопипастить оттуда более детальные спецификации дочерних элементов. Результат в скрипте будет выглядеть так: app.Properties.child_window(title="Contains:", auto_id="13087", control_type="Edit") Само дерево элементов - обычно довольно большая портянка. В некоторых случаях печать всего дерева может тормозить (например, в iTunes на одной вкладке аж три тысячи элементов!), но можно использовать параметр depth (глубина): depth=1 — сам элемент, depth=2 — только непосредственные дети, и так далее. Его же можно указывать в спецификациях при создании child_window. Примеры Мы постоянно пополняем список примеров в репозитории. Из свежих стоит отметить автоматизацию сетевого анализатора WireShark (это хороший пример Qt5 приложения; хотя эту задачу можно решать и без GUI, ведь есть scapy.Sniffer из питоновского пакета scapy). Также есть пример автоматизации MS Paint с его Ribbon тулбаром. Еще один отличный пример, написанный моим студентом: перетаскивание файла из explorer.exe на Chrome страницу для Google Drive (он перекочует в главный репозиторий чуть позже). И, конечно, пример подписки на события клавиатуры (hot keys) и мыши: Благодарности Отдельное спасибо — тем, кто постоянно помогает развивать проект. Для меня и Валентина это постоянное хобби. Двое моих студентов из ННГУ недавно защитили дипломы бакалавра по этой теме. Александр внес большой вклад в поддержку MS UI Automation и недавно начал делать автоматический генератор кода по принципу "запись-воспроизведение" на основе текстовых свойств (это самая сложная фича), пока только для "uia" бэкенда. Иван разрабатывает новый бэкенд под Linux на основе AT-SPI (модули mouse и keyboard на основе python-xlib — уже в релизах 0.6.x). Поскольку я довольно давно читаю спецкурс по автоматизации на Python, часть студентов-магистров выполняют домашние задания, реализуя небольшие фичи или примеры автоматизации. Некоторые ключевые вещи на стадии исследований тоже когда-то раскопали именно студенты. Хотя иногда за качеством кода приходится строго следить. В этом сильно помогают статические анализаторы (QuantifiedCode, Codacy и Landscape) и автоматические тесты в облаке (сервис AppVeyor) с покрытием кода в районе 95%. Также спасибо всем, кто оставляет отзывы, заводит баги и присылает пулл реквесты! Дополнительные ресурсы За вопросами мы следим по тегу на StackOverflow (недавно появился тег в русской версии SO) и по ключевому слову на Тостере. Есть русскоязычный чат в Gitter'е. Каждый месяц обновляем рейтинг open-source библиотек для GUI тестирования. По количеству звезд на гитхабе быстрее растут только Autohotkey (у них очень большое сообщество и длинная история) и PyAutoGUI (во многом благодаря популярности книг ее автора Al Sweigart: "Automate the Boring Stuff with Python" и других). |