Чем популярнее ваше приложение, тем длиннее список устройств, на которых его запускают. В какой-то момент это становится проблемой: некоторые баги загадочным образом воспроизводятся только на конкретной модели, и приходится тестировать продукт на всё большем числе девайсов.
Проблему поддержки множества устройств можно решить с помощью ферм. В докладе объясняется, что это вообще за фермы и как интегрировать их в процесс разработки и тестирования.
— Меня зовут Новиков Павел, я работаю в компании «Новые облачные технологии». Мы работаем над продуктом «Мой офис», делаем Android-версию этого офисного комплекта. Само приложение очень большое. Начну с того, что расскажу, как все устроено в плане архитектуры, чтобы после этого перейти к причине, по которой ферма понадобилась. Вы сможете понять, есть у вас события, когда много кода и внешних зависимостей, или же таких событий нет и нужно ли оно вам. Сможете точнее принять решение о том, что ферма — это классно и нужно. Или нет.
Предыдущий докладчик говорил, что флаттер внутри себя использует SKI. Мы тоже используем, у нас весь натив, все документы полностью рисуются в SKI. Штука очень быстрая, хорошая библиотека.
В центре приложения «Мой Офис Документы» стоит нативное ядро, CORE, которое делается внутри нашей компании, но делается целой командой. Оно написано на чистом С++, чтобы его потом можно было переиспользовать. Переиспользуется оно всеми командами, которые работают над линейкой наших продуктов.
Это десктоп, веб, Android, iOS. Даже Tizen есть, его полноценно и весьма успешно делают.
Самое важное — это что ребята, которые делают CORE, очень тесно завязаны на всех, кто использует этот CORE. Часто происходят ситуации, когда мы просим функционал, который был бы полезен, например, на мобильных клиентах, на iOS, на Android, но на десктопе он не так полезен. Есть такие вещи.
Из этого весьма предсказуемо следует, что иногда появляются баги — вследствие того, что десктоп попросил эту функцию и она внутри себя немного изменила поведение другой функции, которую используют на Android. Это вполне обычная ситуация, и наша задача — побыстрее такие места найти, чтобы помочь ребятам, поработать вместе и побыстрее исправить баги, например, на всех мобильных платформах.
Есть три компонента: нативный CORE, Android-библиотека, в которую этот CORE обернут, и наше Java-приложение. Нативный CORE — чистый С++. Дальше в виде source-кодов он идет в Android-модуль, где сделаны все биндинги для натива, добавлены небольшие прослойки, какая-то логика. Плюс вся отрисовка, которую использует SKI, тоже сделана в этом модуле. Затем модуль как стандартная AR-библиотека вставляется в наше приложение, мы ее используем.
Всякое происходит — могут появляться баги в нативе, в CORE, в биндингах. Тот же unsatisfying link exception, когда неправильно забиндили Java-класс на нативный класс и получили ситуацию, при которой приложение падает во время обращения к нативному методу. И баги у нас в Java-коде.
Самое важное: нам желательно как можно скорее понять, где проблема, чтобы проще было ее решить. Баги, которые вызваны в биндингах, хорошо бы отловить сразу, чтобы они не дошли до UI, ручного тестирования. Это время и довольно простые ошибки, которые несложно найти и исправить.
Чтобы это делать, нужны тесты — самое лучшее, что может быть в данной ситуации. Наша задача — делать код, работающее приложение. Самый простой способ увидеть, что оно работает, — написать для него тесты. Тесты бывают абсолютно разные. Мы пишем все: unit, UI, интеграционные тесты.
Помимо того, что тесты есть, нужно, чтобы они еще запускались. Тесты, которые вы написали, когда разрабатывали приложение, а потом забыли про них и не запускаете, — они лежат, толку от них никакого, а после этого еще код меняется. Прошло два месяца, и мы решили эти тесты запустить, а они даже не собрались. Это неправильно, тесты должны работать постоянно.
Тесты должны регулярно запускаться на CI. Иначе они бессмысленны. Если у вас есть тесты, которые лежат, то у вас нет тестов.
С unit-тестами все довольно просто. Запускаем grlu-тест, таску. Она запускается на CI — все хорошо, прогоняются unit-тесты в вашем приложении, вы видите репорты, отчеты.
Android-тесты запускаются как тесты connect Android, там нужны те же unit-тесты, только они гоняются на девайсах. И тут возникает проблема: UI и интеграционные тесты должны запускаться на реальных устройствах. А CI — не реальное устройство. Можно эту проблему решить несколькими способами.
Например — подключить устройство к серверу CI. Сам я не пробовал, но должно работать, почему нет. У вас есть сервер, он стоит на соседнем столе или под столом, вы к нему подключаете девайсы, девайс с системой их видит, все хорошо, все запускается.
Можно запускать эмулятор на CI. Это довольно рабочий вариант, тот же Jenkins поддерживает плагин, который позволяет запускать эмулятор, но проблема в том, что эмулятор — это, скорее всего, 86-й эмулятор. А если мы говорим про интеграционные тесты, под интеграцией я в нашем случае подразумеваю внешние зависимости, в частности — именно нативный код, потому что у нас очень много нативного кода. И под интеграционными тестами я понимаю тесты, которые проверяют логику «плюсов».
Как итог — сделать это возможно, но не очень удобно. Вариант с подключением устройств напрямую не очень удобный, а эмулятор нам не поможет. Можно использовать древний «армовский», но это так себе идея.
Тут на сцену выходит проект Open STF.
У нас есть много девайсов, давайте их все подключим к одному компьютеру, и научимся ходить на этот компьютер так, чтобы можно было со всеми девайсами работать централизованно.
Выглядит это примерно так. Картинка взята из самого проекта. Список устройств доступен, мы можем подключиться к каждому и работать с ним полноценно. То, что вы видите, — живое устройство, которое подключено к ферме и которым можно полностью управлять через веб-интерфейс, полноценно работать.
Проект Open STF — опенсорсный, и у него есть несколько преимуществ. В первую очередь — работа с реальными устройствами. Как большинство Android-разработчиков, вы понимаете, что ваш код должен проверяться на устройствах. Эмулятор — это хорошо, но есть много вещей, которые нужно проверять на реальных устройствах: тот же натив, работа с SSL. Там много вещей, поведение которых может отличаться. Ферма эту проблему решает.
Что приятно, для работы с этой фермой вам не нужен root на девайсах. Вы просто подключаете девайс к ферме, и он доступен для работы.
Это удобный дебаг. Вам ничто не мешает подключиться по DB к устройству, видимому в сети по простому IP, работать с ним как с устройством, которое будто бы подключено к вашему рабочему компьютеру. Экран устройства вы всегда видите, можете с ним взаимодействовать — только не пальцем, а мышкой.
Устройством могут пользоваться несколько команд. В нашем случае это интересный кейс. У нас обширный парк девайсов в компании и несколько команд, которые с ними работают. Мы — разработчики, тестировщики, все сидим рядом, и девайсы нам нужны для работы. Вторая команда — автоматизаторы, они сидят на другом этаже, но пишут автоматизированные тесты со своим стеком технологий для всех, в том числе для Android. Они тоже имеют доступ ко всем устройствам в компании. Третья — служба поддержки. Им тоже нужно как можно больше устройств. Когда пишут про проблемы в продукте, им нужно их воспроизводить. Проблемы могут быть разные. Преимущество в том, что они имеют доступ ко всем устройствам компании. Это плюс, на устройствах можно запускать приложения и быстрее оказывать поддержку.
Удобно работать удаленно. Приятное следствие, QA так делают: если кто-то приболел или не может выйти в офис, это не значит, что они не смогут работать. Они заходят на ферму и работают как обычно, как если бы девайс лежал рядом с ними.
Мы работаем по скраму, периодически проводим демо. Мы уже примерно год проводим все демо Android-команд только на ферме. Это действительно удобно, когда надо показать несколько фичей, которые будут сделаны, несколько историй, вывести в трансляцию экраны устройств, показать одну историю, следующую историю про планшет, переключиться на другой девайс и показать планшет. Такой подход экономит время и является более удобным, чем с реальными устройствами.
Есть Rest API, можно подумать про автоматизацию.
Все девайсы в порядке, в одном месте, всегда заряжены. Бывает, что нужно на одном устройстве что-то воспроизвести, а оно валяется фиг пойми где, разряжено… Приятный бонус.
Как и у любого проекта, есть недостатки. Не все устройства поддерживаются. Не смогу назвать точных правил. Бывает, подключаете устройство к ферме и оно не определяется. Такое бывает очень редко, у нас был буквально один или два девайса. 95% всего парка работает отлично. Бывает исключение с какими-то китайцами — не определяется и все. Один девайс на 86-м процессоре, фиг знает почему.
Не очень удобно обновлять. К вопросу об обновлении самого продукта STF: поскольку это open source, обновлением в нашей компании занимается команда девопсов. Это не просто нажать кнопку и обновить. Но нет ничего невозможного. Поскольку речь идет про open source, можно облегчить процесс, проблема не критическая.
Выпускать за пределы внутренней сети нежелательно. У нас эта ферма крутится внутри сети, и желательно в интернет ей не светить, потому что ферма позволяет получать, по сути, полный доступ над девайсом, там нет ограничений, вы просто работаете с устройством — можете удалить все что угодно, добавить все что угодно. Если что-то можно сделать с реальным устройством у вас в руке — это можете сделать и с фермой. Так что лучше оставлять его для внутреннего пользования.
Как это выглядит? Внешне есть сервер, где запущена сама ферма, веб-панель, к которой есть доступ.
Эта веб-панель знает о нодах. Каждая нода — компьютер, к которому подключен девайс. Нод может быть несколько — к вопросу о масштабировании. Сами устройства подключаются не туда, где запущена веб-версия, а к нодам. В нашем случае это выглядит так, что две ноды стоят у нас в команде и еще одна — у саппорта, просто потому что им ближе. Физически не все устройства расположены в одном месте, но все доступны через единый интерфейс, который выглядит вот так.
Ко всем устройствам написано, что это за продукт, какая версия ОС, SDK level, какие архитектуры у этого устройства. И его location — провайдер, о котором я говорил. Тут два провайдера. Это наши устройства и устройства саппорта. Последние мы стараемся не трогать, это их устройства, доступные через единый интерфейс.
Сама ферма расположена на GitHub. Первая ссылка — больше рекламная штука.
Там есть все инструкции, как его поднять, запустить, работать с ним. Описаны все плюсы и минусы, проект довольно хорошо документирован.
Проблема была в том, что нужно было как-то подружить эти две вещи. Есть тесты и ферма.
CI-сервер — это любой сервис, который вам больше нравится. Мы используем Jenkins, у меня примеры с интерфейсом про Jenkins, но вы ни к чему не привязаны.
У вас есть STF сервер — сам сервер, провайдер, устройства.
Как их объединять? Очевидно, самый простой способ — Gradle-плагин, который позволяет настроить подключение к ферме при запуске тестов.
Что он умеет? Довольно базовые вещи. Он выберет устройства, которые вам нужны для запуска тестов, подключится к ним до запуска и отключится по завершении, потому что нехорошо держать девайсы залоченными.
Что такое нужное устройство? Через плагин вы можете гибко настроить то, какие именно устройства вам нужны. Вы можете отфильтровать их по названию, взять одни Nexus или Samsung, выбрать количество, которое вы хотите отфильтровать. Это может быть один небольшой набор тестов — вы говорите, что хочу на двух девайсах прогнать и убедиться, что ничего не отломалось. Или nightly-прогон сделать, который все девайсы возьмет, проверит, все запустит, все будет отображаться.
Архитектура. Бывает, нужно запускать тесты на определенной архитектуре. Случаи бывают, но это нужно нечасто.
Провайдер полезен для нас, мы обещаем не трогать девайсы нашего саппорта, чтобы ничего им не испортить, не мешать друг другу. Мы можем сказать: не трогай устройства у саппорта.
Еще полезно сортировать по API-уровню. Если вы хотите зачем-нибудь запустить тест на API 21 и выше — это можно.
Подключается это довольно просто. Как любой Grandle плагин, он интегрируется через подобный синтаксис. Пишете apply plugin, он появится в доступном.
Сейчас сделан следующий шаг. При запуске нужно привязаться к таске запуска тестов, которая будет запускаться на CI, чтобы плагин работал. Сейчас сделано таким образом. Может, и неудобно, но как есть. Улучшить — не проблема. Главное, что можно привязаться к таске connectToSmartphoneTestFarm. Это основная таска, отвечающая за то, чтобы подключиться к девайсам и отпустить девайсы.
Ну и третье — настройка параметров фермы. baseUrl — путь, где ферма расположена. apiKey — ключ, чтобы подключаться по REST, это настраивается в консоли фермы. adbPath — чтобы выполнялась операция adbConnect ко всем устройствам, которые будут найдены. Timeout — системная настройка, по дефолту стоит минута. Она нужна, чтобы ферма сама отпускала девайсы, если они по каким-то причинам не используются.
Так выглядит запуск тестов с использованием фермы. Мы говорим, что connectedDebugAndroidTest запустит все ваши тесты, и сюда передан параметр о том, чтобы не использовать саппорт. Тильда — в данном случае отрицание. Дальше сказать, что я хочу пять устройств, и чтобы они все были –DK21, то есть Lollypop и выше.
Вот как это выглядит при настройке job внутри Jenkins. Тут эти параметры настраиваются и передаются. Это не часть плагина, job на Jenkins нужно сделать самому. Вы можете не указывать все эти параметры, а сделать одну job, в которой они будут заданы железно. И просто кнопка build, если вам не хочется заморачиваться. Может, мы сделаем так же в дальнейшем.
Как итог — после прогона всех тестов вы видите самый стандартный HTML-репорт запуска GUnit, только с одним аспектом: вы будете видеть, что они запускались на разных устройствах. Вы будете видеть названия всех тестов, что вы пробежали, и поймете, что они запустились на каждом устройстве. Вы даже увидите, сколько они запускаются по времени, чтобы из этого в дальнейшем строить анализ, чтобы искать какую-то регрессию. Тут полет для фантазии — можно продумать тест, который один и тот же код запустит сто раз и померяет это. И вы увидите, как код на 86-м или на ARM работает: быстрее или медленнее. Ферма в этом поможет, чтобы можно было не руками подключать, а в автоматическом режиме.
Сам плагин тоже доступен на GitHub, там простенькая документация, но хоть какая. Подключается просто. Весь фидбек приветствуется. Плагин мы написали для себя. Это единственная причина, почему мы не могли нормально ферму использовать. Наконец смогли, порадуемся этому.
Стоит упомянуть, что Gradle есть не везде по объективным причинам. Например, это может быть Appium. Я упоминал ранее, что у нас есть команда автоматизаторов, которые пишут свои тесты на технологии Appium. Там и не пахло Gradle, но им тоже надо использовать ферму.
Это может быть терминал. Есть ферма, на девайсе произошел какой-то краш, и хорошо бы получить log cut с него, скачать файл, что угодно. Что делать? Либо взять девайс и подключить к себе — но тогда теряется вся магия фермы, — либо использовать какой-то дополнительный клиент.
Разработали тулзу простую, которая делает все то же самое, но работает через терминал. Вы так же можете подключаться к устройствам, отключаться, выводить их в список, подключаться, чтобы они были доступны в adb, и эта команда говорит: нужны пять Nexus, когда их найдешь — подключись ко всем. После выполнения команды у вас будут в adb доступны пять устройств. Можете что хотите делать из терминала, тоже удобно. Главное преимущество — это быстрее, нежели делать руками. И тоже доступно на GitHub.
Чисто технически Gradle-плагин и клиент используют нашу библиотеку STL client вместе. Весь сервер написан на Java, есть дальнейшие планы дописать плагин для студии, чтобы девайсы можно было выбирать прямо из UI студии, когда вы работаете. По собственным ощущениям, последние полгода я устройства руками не трогал. Устройства лежат на ферме, я к ним подключаюсь через веб-интерфейс, подключаюсь к adb, копирую путь на ферме и руками девайс не трогаю — лениво. Просто подключился к другому устройству — работаешь с другим.
Особого дискомфорта при разработке я не ощущаю. Потихоньку замечаю, что в нашей команде другие разработчики примерно так же делают. Единственное, QA брыкаются, говорят — неудобно.
Немного в сторону, но тоже про тесты. Я в основном ферму использую в контексте тестирования. UI-тесты — не интеграционные тесты. Кому-то это может показаться капитанством, но отсюда следуют выводы, что UI-тесты могут зависеть от устройств. Я говорю про экспресс-тесты, которые должны запускаться только на смартфонах, которые тестируют экран на смартфоне. На планшете он не имеет смысла, и наоборот. Есть специфичные тесты для планшета, для таблеток. На смартфоне они либо не должны запускаться, либо должны запускаться в каком-то видоизмененном месте. Если вы запустите, вы получите зафейленный тест, и на выходе будет ложноотрицательное срабатывание, что будет мешать. По идее, тесты должны либо проходить, и все хорошо, либо не проходить по объективным причинам. А если они выглядят так, будто прошли, но прошли по необъективным причинам, — это мешает процессу, теряется информативность.
Задача — разделять их. Это то, с чем мы столкнулись, когда начали интегрировать весь процесс работы с фермой и всю описанную автоматизацию. У нас есть как интеграционный тест, так и UI-тесты.
Есть несколько способов. Самые простые известны. И все рабочие, кому как удобнее.
Можно написать свой test runner, который будет анализировать, например, названия классов. Рабочий вариант вполне. Договаривайтесь, что вы именуете классы, которые заканчиваются на TestIntegration или TestUI. Вполне рабочий вариант — test runner это разруливает.
Можно немного пошаманить с Gradle. Складывать тесты в отдельные папки, настроить в Gradle, чтобы он эти папки видел как папки с кодом. На Stack Overflow есть хорошее описание, но я не пробовал.
Можно использовать вариант с JUnit Suit — классом, который позволяет компоновать тесты. Мы остановились на этом варианте, только потому что он самый простой. У него есть недостатки, но с ним проще всего стартануть — не нужно с Gradle шаманить и писать test runner и переименовывать классы, которые у нас есть.
Выглядит следующим образом. Просто обновляете класс — в нашем случае это IntegrationSuite — и перечисляете классы, которые должны запускаться. Сразу минус: их нужно явно указывать.
Когда вы запускаете таску для теста, надо указать отдельным параметром, что я хочу запускать именно данный suite, чтобы запустился только он. Технически так любой тест можно передать, это удобно. Обычно все тесты запустили, они все прошли. В данном случае их можно фильтровать, если вы не используете синтаксис adb instrumentation frame. Все то, что вы видите в студии, когда нажимаете «запустить тесты». Еще это можно шаблонами делать. В данном случае это нужно указать. Тогда будет запускаться только один набор тестов, и он будет выполнять ту изначальную задачу, которую вы перед ним поставили.
При работе Jenkins запустит набор тестов, и мы сможем узнать, сломалась у нас интеграция или нет. Например, пришла новая версия библиотеки от команды CORE. Команда, которая отвечает за то, чтобы интегрировать ее в наши биндинги, их пишет и выкатывает новую версию их библиотеки, которая в формате AR. И нам нужно как можно более простым способом убедиться, что ничего хотя бы не отломалось. Это уже приятно.
Раньше то, что отломалось, падало вообще сходу, но если какой-то метод плохо забиндился или значение какого-то параметра поменялось, то мы узнавали только в самом конце цикла, когда проводилось ручное тестирование — например, в restore. И тут приходят тестировщики и говорят, что раньше работало одним образом, а сейчас немного по-другому — почему? А мы не знаем. Начинаем разбираться. Сначала виним одних ребят, потом других.
В данном случае это делается гораздо быстрее, автоматически. Все артефакты собрались, на Jenkins запустился весь набор тестов. Он подключается к каждому устройству на ферме, и все — мы знаем, что, по крайней мере, все работает как раньше.
Что можно подытожить? Первое — ферма удобна, когда есть несколько команд. Надо сразу понять, что если вы — один разработчик или работаете над продуктом вдвоем, то вам можно особо не запариваться на эту тему. Если у вас нет внешних зависимостей, если вы делаете простенькое приложение, может, оно вам и не нужно. Разбираться с этим, настраивать… Если вы делаете маленький продукт — скорее всего, у вас нет отдельной команды, ответственной за поддержание инфраструктуры, девопсов. Разруливать самостоятельно тоже можно, но удобнее, когда кто-то делает это за вас.
Если у вас такого нет, может, оно вам и не надо. Вы живете, все с вами хорошо.
Другое дело, если у вас появляются внешние зависимости — например, нативный код, причем тот, который постоянно меняется, развивается и дописывается, или у вас идет работа с какими-нибудь секьюрными вещами типа SSL, свои сертификаты, все такое, UI не проверить. Технически можно запустить тесты GUnity на CI или на машине, но в идеале хотелось бы, чтобы такие вещи гонялись на устройствах.
Тесты полезны. Лютое капитанство: чтобы тесты приносили вам пользу, они должны запускаться и работать с наименьшей болью, чтобы не нужно было руками после какого-то действия брать устройство и подключать руками. Желательно, чтобы оно работало, в идеале само, чтобы можно по job запускать. Либо пусть оно хотя бы работает по нажатию одной кнопки «Проверить». Цель в этом.
Все. Это доступно на GitHub, сама ферма с описаниями — как поднять, как настроить, что поддерживается. Плагины доступны, клиенты-плагины.
Этим продуктом пользуемся не только мы — ребята из 2ГИС активно его используют, написали несколько интересных утилит на Python, что тоже позволяет по REST подключаться и выбирать устройство. Мы эту тулзу раньше использовали, но там все не так хорошо.
Интересная фича, которая легко реализуема. Есть тулза, которая позволяет записывать видео с экрана. Поскольку весь экран гоняется по веб-сокетам, вам ничто не мешает по REST получить веб-сокет, узнать, куда подключиться, и получать все ивенты экрана, работать с ними. Мы для себя это не сделали, в отличие от ребят.
Автор: Леонид Клюев