
Всем привет! Меня зовут Вадим Гредасов, я старший системный инженер в отделе автоматизации тестирования IVA Technologies. В этой статье хочу осветить то, как мы в компании организовали автоматизацию тестирования одного из наших продуктов.
Несмотря на то, что значимую роль в автоматизации приложений играет Appium, который тоже используется нами для проверки приложения на ОС Windows и MacOS, рост популярности отечественных систем на базе Linux подтолкнул нас к поиску нового инструмента, так как скорость работы Appium Lunux драйвера нас не устроила. Таким инструментом стал Dogtail.
А теперь немного о нём.
Dogtail — это библиотека для автоматизации тестирования через пользовательский интерфейс (UI) на Linux, которая работает с GTK-приложениями, а также вполне неплохо справляется с Qt-приложениями. Она использует технологии Accessibility (ATK) и DBus для взаимодействия с элементами интерфейса.
Судя по их gitlab репозиторию, проект развивается с 2016 года и неплохо поддерживается авторами, которые охотно отвечают в Issues к проекту.
Основные особенности:
Автоматизация GUI: Dogtail позволяет автоматизировать тесты для приложений, использующих GTK и Qt. Он может взаимодействовать с окнами, кнопками, текстовыми полями и другими элементами интерфейса.
Использование доступности (Accessibility): Dogtail использует доступность (ATK/AT-SPI) для получения информации о компонентах интерфейса. Это делает его особенно подходящим для взаимодействия с элементами, которые поддерживают эти стандарты.
Python API: Dogtail предоставляет Python API, что позволяет разработчикам писать тесты и сценарии автоматизации на Python. Библиотека предоставляет высокоуровневые и низкоуровневые функции для управления интерфейсом.
Настройка хоста
Прежде чем начать использовать Dogtail, необходимо настроить окружение для его работы. Настройки актуальны для Debian ОС.
Установить зависимости
sudo apt install -y build-essential pkg-config libcairo2-dev python3-dev libgirepository1.0-dev python3-pyatspi at-spi2-core
Выяснить установленную версию пакета at-spi2-core
sudo apt-cache policy at-spi2-core
Скачать pyatspi2 и переключить на нужную версию at-spi2-core (имя тега можно узнать на странице проекта)
git checkout PYATSPI_2_38_0 # в примере версия at-spi2-core — 2.38.0
После чего можно добавить модуль pyatspi2/pyatspi к модулям python
sudo cp -r ~/pyatspi2/pyatspi /usr/lib/python3/dist-packages/
Разрешить доступ к экрану
gsettings set org.gnome.desktop.interface toolkit-accessibility true
Таким образом хост будет готов к использованию Dogtail.
Архитектура тестов
Паттерн тестов
Для тестирования нами был выбран классический паттерн — page object. Но так как у нас для тестирования приложения на разных ОС используются два разных инструмента (Appium и Dogtail), то наша реализация page object имеет свои особенности.
Начнём по порядку.

Особенность page object выражается в добавлении нового уровня абстракции — класса Instrument, в нём, как раз, определяется через что мы взаимодействуем с приложением. Таким образом задачи, решаемые на каждом уровне абстракции, выглядят следующим образом:
Класс Instrument становится низкоуровневым, забирая эту роль у _PBase. В нём определяются методы, в которых используется тот или иной инструмент для взаимодействия с приложением. Такие как поиск элементов, различные клики, движения мышью и тд.
Класс _PBase описывает основные методы взаимодействия со странницей приложения, свойственные всем дочерним страницам, только использует для этого новый класс Instrument.
Классы для каждой страницы приложения описывают методы свойственные только для своей страницы с использованием методов из _PBase.
Классы страниц для конкретной платформы (ОС) описывают методы характерные только определённой платформы.
Для работы с окнами приложения, которые могут присутствовать на разных страницах, реализован паттерн chunk object - частный вариант page object. Эти классы так же наследуются от _PBase.
Отдельно стоит упомянуть хранение локаторов для страниц. Они разделены по разным платформам, плюс общие - свойственные всем платформам.
В конечном итоге страница для определённой ОС, описывающая логику работы с приложением, наследуется от:
общей страницы для всех платформ, а та, в свою очередь, от _PBase;
класса с локаторами для этого типа платформы.
Теперь посмотрим, как это выглядит в коде.
Код тестов
Стоит упомянуть какие стратегии поиска используются в нашем тестировании — это обычный xpath элемента для Appium и название поля элемента для Dogtail.
Примеры локаторов:
Windows
class LLandingMeeting:
@staticmethod
def LANDING_MEETING_PAGE_PRIMARY_ELEMENT(lang=''):
return 'xpath', '//*[contains(@Name,"nameText")]'
Linux
class LLandingMeeting:
@staticmethod
def LANDING_MEETING_PAGE_PRIMARY_ELEMENT(lang=''):
return 'name', 'conferenceLandingPage'
Если с xpath всё стандартно, то для Dogtail стоит пояснить, что 'name' — это один из атрибутов элемента, а 'conferenceLandingPage' - его значение.
Вот как это может выглядеть в UI инспекторе.

Теперь перейдём к описанию классов.
class Instrument:
def init(self, driver):
self.driver = driver
self.instrument = 'selenium' if isinstance(self.driver, appium.webdriver.webdriver.WebDriver) else 'dogtail'
self.parent_element = None
Тут стоит пояснить, что parent_element является нашей фичёй для работы с Dogtail, которая позволяет сократить время поиска элементов на странице и сузить зону поиска элемента.
А так выглядит один из основных методов для поиска элемента:
def find_element(self, locator):
strategy, value = locator
if self.instrument == 'selenium':
return self.driver.find_element(strategy, value)
else:
if self.parent_element is None:
return self.driver.child(name=value)
else:
return self.parent_element.child(name=value)
Здесь мы видим, что локатор для поиска содержит в себе стратегию, в нашем случае это xpath для работы с Appium или название поля для Dogtail, значение — это путь до элемента при работе через Appium и значение поля для Dogtail.
У полученного через Dogtail элемента есть атрибуты showing и visible. Атрибут showing указывает на то, отображается ли элемент на странице или нет, visible указывает виден ли элемент пользователю. Это удобно в случае, если страница с элементом отобразилась, но, чтобы увидеть элемент нужно скролить страницу.
Теперь сам класс с базовой страницей.
class _PBase:
def init(self, driver, lang='', check_primary_element=True):
self.driver = driver
self.instrument = Instrument(self.driver)
self.instrument.implicitly_wait(0)
if check_primary_element:
self.wait_presence(self.primary_element)
Тут мы видим создание экземпляра класса Instrument, который будет использован, например, при получении элемента:
def get_object(self, locator, multiple=False):
self.wait_presence(locator)
meth = self.instrument.find_elements if multiple else self.instrument.find_element
try:
result = meth(locator)
except (NoSuchElementException, SearchError):
log.warning(f"Element with locator '{locator}' doesn't exist")
raise
return result
Аналогичным образом выстраиваем остальные методы взаимодействия с приложением, используя экземпляр класса Instrument.
Теперь об остальных страницах.
Для каждой страницы мы определяем свой родительский элемент (parent_element), о чём было упомянуто выше, от которого и происходит поиск остальных элементов, а также основной элемент страницы (primary_element), что даёт нам понять, что мы находимся на ней.
С этого момента поподробнее.
О parent_element. Это элемент, который содержит в себе все остальные элементы (в атрибуте child), характерные для данной страницы, и с которыми мы хотим взаимодействовать. Он определяется следующим образом:
class LLogin:
@staticmethod
def LOGIN_PAGE(driver):
return (driver.child(name='appStackView')
.child(name='welcomePage')
.child(name='loginPage')
.child(name='flickable')
.child(name='contentView')
.child(name='loginItem'))
И при объявлении класса страницы, в данном примере с логином, мы записываем в parent_element тот элемент, от которого будут искаться все остальные. То есть, до нашей страницы с логином мы как бы прописываем путь от родительского элемента ('appStackView') к дочернему ('loginItem'). В этом дочернем элементе, в нашем случае, будут содержаться элементы: 'loginInput', 'passwordInput' и 'submitBtn'.
class PlatformLogin(PBase):
def init(self, args, *kwargs):
self.lang = kwargs['lang']
self.primary_element = self.L_TEXTFIELD_LOGIN(self.lang)
super().__init__(*args, **kwargs)
if self.get_platform() == "Linux":
self.parent_element = self.LOGIN_PAGE(self.driver)
self.set_parent_in_instrument()
Метод set_parent_in_instrument прокидывает найденный элемент в экземпляр класса Instrument.
И напоследок, как запустить приложение.
import dogtail.config
from dogtail import tree
from dogtail.utils import run
def start_dogtail(app_path, app_name, timeout, debug):
dogtail.config.config.logDebugToStdOut = debug
dogtail.config.config.logDebugToFile = debug
run(app_path, timeout=timeout, dumb=False)
driver = tree.root.application(app_name)
return driver
В driver будет содержаться объект Atspi.Accessible, с которым теперь можно взаимодействовать.
Параметры запуска
app_path — путь к исполняемому файлу приложения.
app_name — название приложения.
timeout — таймаут запуска приложения.
dumb — флаг для режима без терминала (dumb mode).
debug — флаг, который включает вывод отладочной информации в стандартный вывод и в файл.
Пример теста
import pages
def test_login():
lang = setup_aut['setup']['cmd']['lang']
driver = setup_aut['driver']
config = setup_aut['setup']['config']
pre_login_page = pages.get_page('pre_login')(driver=driver, lang=lang, check_primary_element=True)
pre_login_page.select_server(config['server'], confirm_address=True)
login_page = pages.get_page('login')(driver=driver, lang=lang, check_primary_element=True)
login_creds = setup_aut['setup']['config']['credentials']['user1']
login_page.login(**login_creds)
main_page = pages.get_page('main_page')(driver=driver, lang=lang, check_primary_element=True)
Что происходит?
Из строки запуска тестов получаем язык приложения, и сам драйвер для работы с приложением.
Затем используем страницу выбора сервера, вызываем у неё метод, который вводит адрес сервера, полученного из файла конфигурации.
Используем страницу логина чтобы авторизоваться, вызывая у неё метод login с переданными в него параметрами авторизации.
После чего объявляем главную страницу приложения и проверяем с помощью аргумента check_primary_element, что мы на ней.
Удалённое взаимодействие с приложением
В некоторых кейсах нам потребовалось использовать два клиентских приложения, что заставило нас сделать удалённый клиент. Это мы реализовали через Python библиотеку RPyC. Подробнее о реализации этого метода написано тут.
Заключение
Плюсы
Скорость взаимодействия с приложением оказалась существенно выше (более чем в 2 раза), чем при аналогичном подходе с использованием Appium и его Linux драйвера
Невысокий порог вхождения в понимание работы данного инструмента
Поддержка Dogtail со стороны разработчиков, быстрая реакция на заведённые Issues
Совместимость с Linux, особенно актуально для отечественных ОС
Минусы
Невозможно обращаться к элементам через xpath
Закрытие приложения невозможно средствами Dogtail, а только через библиотеку psutil либо через pkill
Инструмент подходит только для Linux
По сравнению с Appium плохо освещён в сети, особенно в русскоязычном комьюнити
Доступен только для разработки на Python
Несмотря на свои недостатки, в нашем случае инструмент хорошо подошёл для проверки приложения на таких ОС как Astra, Alt, AlterOS, что делает его более выигрышным вариантом за счёт совместимости с выбранными ОС и скорости прохождения тестов.