Как стать автором
Обновить
33.66

Автоматизация тестирования desktop приложений с помощью Dogtail

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров1.4K

Всем привет! Меня зовут Вадим Гредасов, я старший системный инженер в отделе автоматизации тестирования IVA Technologies. В этой статье хочу осветить то, как мы в компании организовали автоматизацию тестирования одного из наших продуктов.

Несмотря на то, что значимую роль в автоматизации приложений играет Appium, который тоже используется нами для проверки приложения на ОС Windows и MacOS, рост популярности отечественных систем на базе Linux подтолкнул нас к поиску нового инструмента, так как скорость работы Appium Lunux драйвера нас не устроила. Таким инструментом стал Dogtail.

А теперь немного о нём.

Dogtail — это библиотека для автоматизации тестирования через пользовательский интерфейс (UI) на Linux, которая работает с GTK-приложениями, а также вполне неплохо справляется с Qt-приложениями. Она использует технологии Accessibility (ATK) и DBus для взаимодействия с элементами интерфейса.

Судя по их gitlab репозиторию, проект развивается с 2016 года и неплохо поддерживается авторами, которые охотно отвечают в Issues к проекту.

Основные особенности:

  1. Автоматизация GUI: Dogtail позволяет автоматизировать тесты для приложений, использующих GTK и Qt. Он может взаимодействовать с окнами, кнопками, текстовыми полями и другими элементами интерфейса.

  2. Использование доступности (Accessibility): Dogtail использует доступность (ATK/AT-SPI) для получения информации о компонентах интерфейса. Это делает его особенно подходящим для взаимодействия с элементами, которые поддерживают эти стандарты.

  3. 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, что делает его более выигрышным вариантом за счёт совместимости с выбранными ОС и скорости прохождения тестов.

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии2

Публикации

Информация

Сайт
iva.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия