Pull to refresh

Imagrium: Фреймворк для автоматизации кросс-платформенного тестирования мобильных приложений

Reading time10 min
Views6.7K
Компания, в которой я работаю, разрабатывает ПО на заказ, в том числе мобильные приложения на базе Android и iOS. В связи с тем, что конкуренция в этом сегменте рынка довольно высока, тестировщики не только отвечают за соответствие конечного продукта спецификациям и ожиданиям клиента, но еще и поставлены в жесткие рамки по бюджету и срокам тестирования. Это побуждает нас исследовать новые инструменты и методы, которые позволили бы нам уменьшать затраты на тестирование и повышать качество продуктов.

Imagrium — это результат одного из таких исследований. Технически это Jython фреймворк для кросс-платформенного тестирования мобильных Android/iOS приложений с помощью распознавания изображений, написанный нашей компанией. Он представлен в виде рабочего PyDev проекта, который вы можете изменить под свои нужды. Код распространяется под MIT лицензией и доступен на Github. В этой статье я расскажу о принципах работы фреймворка и его устройстве.

Принципы работы


Фреймворку уже около 2-х лет, в течение этого времени он рос и развивался, вбирая в себя опыт применения на боевых проектах. При этом основные принципы, пожалуй, не сильно изменились. Они таковы:

Использовать одни и те же тесты на разных платформах

Обычно наши клиенты заказывают приложение сразу под несколько платформ, чаще всего Android и iOS. Получается, что спецификация одна, функциональные тесты одинаковые, так что наиболее эффективным с нашей точки зрения было бы иметь одну тестовую базу для разных платформ. Другими словами, один и тот же тест должен проходить под разными платформами.

Отделять ресурсы от логики

Инструменты, которые позволяют кросс-платформенное тестирование, можно разделить на два типа:
  • Предоставлющие общее API и сервис-посредник, который транслирует общие вызовы в специфичные для оси (MonkeyTalk, Appium, Robotium).
  • Использующие метод распознавания образов (Borland Silk Mobile, eggPlant).


Мы большие фанаты первого подхода, но на тот момент некоторых инструментов еще не было, остальные работали не очень стабильно, это и вынудило нас попробовать свои силы в написании альтернативы. При этом мы не хотели очередной record-and-replay инструмент, потому что тесты, созданные подобными инструментами, очень быстро становится очень сложно содержать. Почему? Потому что в них ресурсы связаны с логикой работы. Например, при изменении какой-нибудь примитивной операции придется не только переправлять картинки во всех текстах, но еще и менять в нужных тестах нужные шаги. Хотелось избежать подобной бессмысленной и затратной работы, используя популярный паттерн PageObject.

Поддерживать непрерывную интеграцию и удобную отладку

Все наши проекты автоматически собираются Jenkins, так что с первой же строчки кода этого фреймворка нам хотелось бы, чтобы и тесты под приложение тоже можно было запускать автоматически. Изначально это было не так просто, потому что для работы с изображениями решили использовать Sikuli (как наиболее популярное, документированное и бесплатное решение), у которой на тот момент не было простой возможности отделить библиотеку от IDE и была поддержка только Jython 2.5 (у которого, между прочим, не было еще поддержки json, да-да), в unittest не было кучи вкусных возможностей (например, авто-нахождения тестов). Однако со временем эти трудности были побеждены, и теперь результаты тестов доступны в jUnit формате, а Ant делает из них красивую страничку со статистикой.

По второму пункту, если вы видели Sikuli IDE, то вы понимаете, что делать в ней что-то чуть более серьезное, чем 10-ти строчное приложение — это боль. Хотя бы потому, что там нет отладчика. Для нас этого было достаточно, чтобы перейти на использование PyDev Eclipse, который привычен программистам и содержит массу вспомогательных возможностей для ускорения разрабоки.

Гарантировать одинаковое начальное состояние системы для всех тестов

Мы позаимствовали идею независимости тестов друг от друга из jUnit т.к. это дает возможность запускать тесты параллельно или совершать выборочные проверки. Еще одна задача, которая нас к этому подтолкнула — это падение тестов в середине выполнения. Нам нужна была система, в которой падение одного теста никак не отражалось бы на выполнении других тестов. В результате решили использовать snapshots (снимки) эмулятора на Android и функцию reset симулятора на iOS.

Загрузка и ответы эмулятора не должны критически задерживать выполнение тестов

Нам очень нравилась скорость iOS симулятора, чего нельзя было сказать про коробочный Android эмулятор. У нас были надежды на HAXM и x86 образы, поставляемые Intel, но беда в том, что эти образы до версии 4.4 не содержали Google API, который используется в большинстве наших приложений (т.е. приложения просто не ставились на эти образы). В свою очередь образ 4.4, который содержит это API, работал у нас нестабильно (например, мог упасть при повторной установке приложения). Поэтому мы выбрали Genymotion и VirtualBox для создания снимков и управления ими.

Если вы разделяете эти принципы и думаете о каком-то подобном фреймворке для тестирования, предлагаем вам рассмотреть наш как одну из альтернатив.

Требования к окружению


Imagrium успешно работает на Java 7 x64 и Windows 7 x64 (ветка win репозитория) или MacOS 10.9 (ветка ios репозитория).
Исторически так сложилось, что под Windows тестировался только Android, а под MacOS, соответственно, только iOS.

Демонстрация работы


Перед тем, как мы кратко пройдемся по правилам написания тестов на Imagrium, хотим показать видео с возможностями фреймворка (выноски на английском). На этом видео показано прохождение теста для приложения HopHop на iOS и Android.


Как писать тесты


Код, написанный на Imagrium, можно разделить на два блока: страницы и тесты. В этой группировке тест — это последовательность операций, проводимых на разных страницах, а также переходы между страницами. Например:

authPage = AuthPage.load(AppLauncher.box, self.settings)
fbAuthPage = authPage.signUpFb()
fbAuthPage.fillEmail(self.settings.get("Facebook", "email"))
fbAuthPage.fillPassword(self.settings.get("Facebook", "password"))
fbConfirmPage = fbAuthPage.login()
lobbyPage = fbConfirmPage.confirm() 


Как можно догадаться из кода, сначала тест загружает страницу AuthPage, потом переходит с нее на fbAuthPage, заполняет необходимые данные и отправляет форму, подтверждает пользователя и передит на lobbyPage. Другими словами, тест ходит по страницам и выполняет понятные с т.з. тестировщика операции, оставляя реализацию операций внутри страниц. Т.е. тесты — это достаточно простая группа, и чтобы научиться их писать, мы должы всего лишь научиться писать страницы. Это тоже достаточно просто.

Пишем страницы


Страница — это Jython представление экрана/activity/страницы приложения. Технически это класс с полями и методами, которые управляют этими полями. В самом сложном случае он выглядит так:

class FbAuthPage(Page):

    email = ResourceLoader([Resource.fbEmailFieldiOS, Resource.fbEmailFieldiOS_ru])
    password = ResourceLoader([Resource.fbPasswordFieldiOS, Resource.fbPasswordFieldiOS_ru])
    actionLogin = ResourceLoader([Resource.fbLoginBtniOS, Resource.fbLoginBtniOS_ru])
        
    def __init__(self, box, settings):
        super(FbAuthPage, self).__init__(box, settings)
        
        self.email = self.box
        self.password = self.box
        self.actionLogin = self.box
        
        self.settings = settings
        self.waitPageLoad()

        self.checkIfLoaded(['email', 'password'])
        
    def fillEmail(self, text):
        self.email.click()
        self.waitPageLoad()
        self.inputText(text)


В этом фрагменте используется большинство вкусностей Imagrium, так что давайте детально обсудим этот фрагмент с т.з. возможностей фреймворка.

Определение полей и локализация


Начнем с этой строки:

    email = ResourceLoader([Resource.fbEmailFieldiOS, Resource.fbEmailFieldiOS_ru])


Этот фрагмент связывает поле страницы email с графическим ресурсом (картинкой или строкой). В данном случае мы связываем поле сразу с двумя ресурсами (для английской и русской локали). Когда страница спрашивает поле email, система пытается найти одно из этих изображений и возвращает область, связанную с первым успешно найденным изображением.

По нашим договоренностям мы храним графические ресурсы в директории res. Чтобы задать ресурс, мы должны передать в ResourceLoader путь до ресурса или текст, например:

    email = ResourceLoader("res/pages/ios/fb_auth/fbEmailFieldiOS.png", "Password")


но для гибкости и удобства содержания кода мы храним пути до ресурсов в переменных объекта src.core.r.Resource.

Итого: Чтобы управлять графическим элементом или строкой, нужно добавить в соответствующую страницу объявление ResourceLoader.

Инициализация страницы и проверка полей


Объявление поля только задает необходимую нам связь, чтобы использовать ее, нам нужно обратиться к полю. Такие обращения происходят либо при использовании какой-нибудь операции на поле (например, перетаскивание или щелчок), либо при инициализации страницы. Последняя операция очень важна, потому что во-первых она позволяет задать область поиска для полей (обычно мы хотим сузить ее до границ эмулятора) и проверить, что мы именно на той странице, на которой хотим. Поэтому давайте рассмотрим подробнее, что происходит в коде инициализации страницы на примере FbAuthPage.

Во-первых, мы всегда должны наследовать страницу от класса src.core.page.Page
class FbAuthPage(Page):

так как это дает нам доступ к общим методам управления страницами, например, метод ожидания полной загрузки страницы. Также мы должны запустить родительский конструктор при инициализации страницы.
    def __init__(self, box, settings):
        super(FbAuthPage, self).__init__(box, settings)


и последняя особенность инициализации — возможность задать для поля область поиска, обычно это область, занятая эмулятором, и чтобы искать только в ней, мы пишем
        self.email = self.box
        self.password = self.box

Параметр box изначально считается после создания снимка эмулятора и перед запуском приложения а первый раз, а дальше передается от страницы к странице. Более детально, берутся две линии (вертикальная и горизонтальная на эмуляторе) из, например, res/pages/android/hdpi/core, система обнаруживает их на странице и строит по ним область эмулятора. Если мы ничего не присваиваем полям, система ищет эх по всему экрану (что обычно сказывается на качестве поиска), хотя это бывает нужно для какой-нибудь экзотики, например, чтобы нажать какую-нибудь hardware кнопку на эмуляторе.

Обычно при инициализации страницы мы проверяем, что на ней находятся нужные нам поля, и делаем это таким методом:
self.checkIfLoaded(['email', 'password'])


Некоторые поля могут быть невидимы при инициализации, указывайте только видимые. Если страница не может найти поля, она посылает исключение AssertionError которое приводит к провалу теста а также сообщает детали в stdout.

Итого: Если вы хотите, чтобы система искала поля в рамках эмулятора, присваивайте им self.box. Используйте проверку полей страницы с помощью self.checkIfLoaded().

Что можно делать с полями


В нашем примере кода FbAuthPage мы использовали метод click() на поле email:
self.email.click()


Каждое поле на самом деле представлено объектом Match из Sikuli, так что с ним можно делать все, что написано в соответствующей спеке (таскать, щелкать, зажимать, отпускать, вводить текст, и прочее).

Доступ к конфигурации


Конфигурация является очень важной частью запуска тестов. В частности она определяет, под какую платформу мы хотим запустить тесты, какие тесты, для какого приложения. Также в конфигурацию можно прописывать свои переменные и использовать их в тестах или в коде страниц.

В нашем примере класса FbAuthPage вы можете увидеть строку:
self.settings = settings


Здесь аттрибут settings это образец ConfigParser связанный с текущим конфигурационным файлом, так что вы можете использовать все методы из официальной спеки при работе с ним. Imagrium добавляет settings к каждому тесту, так что вы можете использовать конфигурацию и непосредственно в тестах.

Пример работы с конфигурацией:

self.settings.get("Facebook", "email")


ОС-зависимые методы


Например, временами требуется нажать кнопку back (специфично для Android) или ввести текст (у Sikuli это не получается сделать просто, потому что она вводит текст асинхронно, что приводит обычно к тому, что часть символов не успевает дописаться). Для предоставления этих возможностей и были введены классы, которые дают вашей странице ОС-специфичный функционал.

На практике это выглядит так (множественное наследование!):

class FbAuthPageiOS(FbAuthPage, iOSPage):


или так:

class FbAuthPageAndroidHdpi(FbAuthPage, AndroidPage):


Итого: Если хотим ОС-специфичные функции, наследуемся от нужного класса.

Организация страниц


В предыдущих разделах мы говорили сначала о FbAuthPage, а потом перескачили на FbAuthPageiOS и FbAuthPageAndroidHdpi. В этой секции мы обсудим, что это за классы, зачем они нужны, и как связаны с FbAuthPage.

Изначально мы говорили, что хотим запускать один и тот же тест на разных платформах, но даже в рамках одной платформы у нас могут вознинуть серьезные отличия в представлении графических ресурсов. Например, ресурсы под hdpi могут отличаться от xhdpi, ресурсы под iOS могут отличаться от тех же под Android. При этом отличаются только ресурсы, а методы работы с ними остаются теми же (или почти теми же, с поправкой на guidelines). Нужно было придумать какое-то решение для переопределения ресурсов под разные платформы, и мы использовали стандартное наследование. Другими словами, наши страницы можно разделить на два уровня — общие и платформозависимые.

  1. Общая страница содержит общую логику работы с ресурсами на странице. В нашем примере это FbAuthPage. Эти методы делают те же операции и под iOS, и под Android.
  2. Платформозависимые страницы обычно содержат только ресурсы, которыми нужно переопределить ресурсы общес страницы. Они выглядят как-то так:
    class FbAuthPageAndroidHdpi(FbAuthPage, AndroidPage):
        email = ResourceLoader([Resource.fbEmailFieldAndroidHdpi, Resource.fbEmailFieldAndroidHdpi_ru])
    



При таком разнообразии страниц, не хотелось бы, чтобы тест или страница сама решала, какие страницы им загрузить. В Imagrium это обязанность системы — прочитать конфигурацию и загружать нужные страницы, когда страница вызывает метод load(). Чтобы система загружала правильные классы, эти классы должны быть по-определенному названы. Более подробно:

  • iOS страницы должны называться [общая страница] + «iOS», например: FbAuthPageiOS. Дополнительно эта страница должна быть наследником FbAuthPage.
  • У Android страниц больше параметров — это версия ОС и плотность. Сначала система пытается загрузить класс [общая страница] + "_" +"[мажорная версия]" + "_"+"[минорная версия]" + "_" + «Android» + "[плотность]". Например: FbAuthPage_4_2_AndroidHdpi. Если она не нашла такой класс, то пробует загрузить: [общая страница] + «Android» + "[плотность]". Например: FbAuthPageAndroidHdpi.


Если все еще класс не найден, то система отдает AssertionError с соответствующим описанием, что проваливает тест.

На практике это выглядит так:

В тесте пишем
fbAuthPage = authPage.signUpFb()


В общей странице AuthPage есть метод:
def signUpFb(self):
    self.actionAgreeTermsBtniOS.click()
    self.actionSignUpFb.click()
    return FbAuthPage.load(self.box, self.settings)


Данный метод вызывает метод load(), при выполнении которого система решает, какую бы из страниц вызвать(например, FbAuthPageiOS или FbAuthPageAndroidHdpi).

Итого: Мы реализуем общую логику работы страницы, а потом, если того требует другая плотность/платформа, добавляем измененные ресурсы в соответствующие классы страниц. Система по конфигурации решает, какую страницу вызвать в подходящем случае.

Как запускать тесты


Предполагается, что вы клонируете проект с Github, откроете в Eclipse PyDev, удалите все лишнее, добавите все нужное, а потом захотите это протестировать. Для тестирования вам необходимо установить все, что нужно для работы фреймворка (смотрите инструкции по установке), а потом из корня проекта запустите:

ant


Чуть более подробно, этот вызов запускает run.py с определенным конфигурационным файлом в качестве единственного входного параметра (например, conf/android_settings.conf) а также с прописыванием необходимых путей в переменные PATH и CLASSPATH, а потом создает страницу с результататми тестирования.

В общем случае при тестировании запускается эмулятор, на нем переустанавливается приложение, после чего создается снимок эмулятора. Далее на каждый тест запускается один и тот же снимок, на котором и проходит очередной тест.

Заключение


Мы рекомендуем использовать Imagrium в тех случаях, когда вам нужно тестировать сразу несколько платформ или когда у вас нет простого доступа к коду приложения, чтобы добавить в нем локаторы для GUI элементов. Наиболее быстро освоить фреймворк получится у тех, кто программировал на Python, хотя простота синтаксиса языка должна способствовать быстрому обучению работы с инструментом. В этой статье были приведены только основы работы с Imagrium, подробно о конфигурации фреймворка и его возможностях (например, многопользовательское выполнение сценариев) я расскажу в последующих статьях, если эта статья будет интересна сообществу. Также можете прочитать официальную документацию на Github странице проекта и посмотреть этот Hello, World пример.
Tags:
Hubs:
Total votes 10: ↑8 and ↓2+6
Comments6

Articles