Тестировать андроид — сложно. Автоматизированно тестировать андроид — очень сложно. А если автоматизированно тестировать 6 очень разных приложений на 10 разных версиях ОС Android с использованием 3 языков программирования, используя внутреннюю систему CI/CD, которая написана для десктопных платформ, то это проходит по разряду «медленно и за очень много денег».



Меня зовут Сергей Павлов, и я работаю в команде Mobile Solutions Testing «Лаборатории Касперского» на позиции Senior Software Development Engineer in Test (SDET), где совмещаются навыки разработчика, тестировщика и DevOps. Я расскажу, как у нас получилось создать инфраструктуру на пользовательских десктопах, способную относительно стабильно и быстро запускать до 8 эмуляторов Android на машине. А также как мы запаковали практически все в Docker и научились грамотно разделять потоки тестов.

Зачем тестировщикам строить инфраструктуру


Как правило, тестировщики занимаются либо ручным, либо автоматическим тестированием. Мы в мобильном штабе автотестируем несколько продуктов:



Наш флагманский продукт — Kaspersky Internet Security — позволяет сканировать сайты и сеть на предмет malware. Kaspersky Safe Kids — родительский контроль, контролирующий сайты, которые можно посещать ребенку, и ограничивающий доступ к приложениям. Kaspersky Password Manager обеспечивает безопасное хранение паролей и документов. И многое другое.

Все эти приложения специфичны в том смысле, что это не просто приложения под Android, в которые зашел, посмотрел на карту и вышел. Во всех предусмотрены взаимодействия с системой. Настройки антистилера в KIS требуют включения администратора устройства, интернет-фильтр включается в accessibility, для отображения окон в SafeKids требуется разрешение на DrawOverlay и так далее.

В автотестах нужно сначала выдать все эти разрешения приложению, а потом еще и протестировать.

Автотестов у нас много. Основные три вида:
  • unit;
  • integrational;
  • UI.

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



Само собой, у нас есть Unit-тесты, и они пишутся на стандартных для Android Java и Kotlin.

Когда я пришел в «Лабораторию Касперского», Appium + ruby + cucumber был основным инструментом для ui-автоматизации. Поскольку задача заключалась в Blackbox-тестировании, а точнее в создании автотестов для приемки и регресса, такой подход выглядел наиболее удобным. Тесты писались QA Automation и просто QA. Тесты в принципе были наполнены взаимодействиями с системой на девайсе, загрузкой всего необходимого с агента на девайс, запросами к инфраструктуре приложения и даже взаимодействием с веб-порталом через Selenium.

Есть набор инструментов Appium + java + cucumber. По аналогии с автоматизацией на ruby коллеги-разработчики решили попробовать автоматизировать на java, набросали общую структуру проекта и написали несколько тестов. А затем мы подключили стажеров. Им в качестве старта для освоения языка предоставили проект — и они довольно успешно написали большое количество тестов. Кстати, участники этого эксперимента затем перешли на новую ступеньку карьерной лестницы: некоторые из них перешли в разработчики, а кто-то решил стать сдетом.

Связка Appium + python + pytest изначально решала задачу тестирования SDK для антивирусных продуктов: на девайс загружались тестовые семплы и проверялась работа антивируса. Но затем это разрослось в полноценную связку для тестирования и готовых продуктов

И наконец, Kaspresso — вы, наверное, много слышали про эту технологию. Разработчики решили, что нужно покрывать новые фичи автотестами, и решили сделать удобный фреймворк. Получилось круто, и в принципе это уже почти стандарт в индустрии. Если не слышали, то можете ознакомиться с Kaspresso на GitHub.

С чего все начиналось


Изначально автотестов было мало. И тогда мы решили попробовать запускать их на пайплайнах. Нашли старенький Mac mini, а также десктоп под столом одного из разработчиков, и подняли Jenkins. На Mac mini мы все собирали, на десктопе гоняли тесты. Все было спокойно.



Но тестов становилось больше. Вместо прогона 10 тестов суммарно на 15 минут начали прогонять 100 тестов, потом 150, а затем 500. Сейчас только у одного продукта в пайплайне более 500 акцептов. Нам стало не хватать мощностей одного десктопа. Когда необходимо масштабироваться и параллелить тесты, сразу возникают вопросы: как правильно выстроить инфраструктуру для андроид-автотестов (на чем их гонять — на эмуляторах или живых устройствах), как одновременно запускать несколько тестов в рамках одной машины и как эту машину настраивать. Кажется, что достаточно докупить серверов, добавить ресурсов, и все будет работать. Но нам понравилась идея десктопов.

Понятно, что выбор пал в пользу десктопов не просто так. Но разговор, почему именно десктопы, мы начнем издалека.

Что такое эмулятор


Мобильным UI-автотестам для запуска нужно устройство. Но покупать 100 Google Pixel, чтобы запускать 500 тестов, — невозможно. Их батарея вздувается, сами устройства ломаются, их нужно перепрошивать и поддерживать. Поэтому мы используем эмуляторы.

По своей сути эмулятор — это виртуальная машина особого вида с Android. Ее нельзя запустить через VirtualBox или обычный Hyper-V (во всяком случае, без солидных приседаний). Нужно использовать именно Android Virtual Device — приложение для запуска эмуляторов разных Android-систем, представляемое компанией Google. Образы разных Android-ов, начиная со старых 4.4, которые были актуальны еще лет семь назад, и заканчивая самыми новыми Android 14, есть в свободном доступе.

Так как это не обычный гипервизор, эмулятор и запуск тестов на нем имеют свои потребности. Как и любой виртуальной машине, эмулятору нужен CPU помощнее, потому что ему приходится пахать за двоих, нужно побольше RAM, потому что на эмуляции полноценного телефона приложения тоже должны висеть в памяти, сворачиваться, разворачиваться, паузиться, закрываться, подниматься и так далее. А кроме того, нужно отрисовывать все это.

Работая с обычным телефоном, вы наверняка замечали, что везде есть какие-то анимации — сообщения вылезают со всех сторон, окна открываются и закрываются. Все это нужно рисовать.

У эмуляторов Android есть headless-режим, но по факту он не отключает отрисовку. Это режим без экрана. Рендеринг при этом все равно идет и грузит CPU. Если с помощью ADB — еще одной утилиты, которую предоставляет Google для работы с Android-устройствами, — в headless-режиме сделать скриншот через Android Debug Bridge (эта утилита для мобильного тестирования и разработки подключается к девайсу не как админ, но с расширенными правами, и проводит с устройством различные манипуляции), мы получим полный UI.

Все эти потребности неслабо нагружают основную машину. И если с CPU и RAM у нас вариантов нет, то с GPU есть интересный момент: можно рисовать, используя мощности CPU, а можно задействовать GPU, то есть видеокарты.



CPU не очень хорошо справляется с задачами отрисовки, так как процессор создан не для этого. Видеокарта справляется с подобными задачами гораздо лучше.

Мы пробовали оба варианта. Мы взяли два десктопа, в один воткнули видеокарту, в другой — нет, и начали смотреть, что происходит. График ниже показывает два варианта отрисовки: на CPU (оранжевая линия) и на GPU (синяя линия).



До первой минуты графики идут более-менее одинаково. Это момент поднятия эмулятора, когда он запускается с нуля, начинает подгружать систему, UI и все, что у него есть. Далее эмулятор приходит в рабочее состояние, и мы видим, что в случае с отрисовкой за счет видеокарты нагрузка на CPU резко падает — в этот момент все расчеты просто увели на видеокарту. Таким образом, переведя часть расходов с CPU, можно сэкономить процессорное время.

Почему не серверы


Представим, что нам нужно расширить инфраструктуру. Мы можем купить или арендовать в ДЦ серверы (в данном случае неважно, покупка или аренда) — чистые CPU, RAM и SSD. На графике ниже нам как раз выделили один сервер на 48 ядер (с гипертредниками — 96 потоков), 96 ГБ RAM и 1 ТБ SSD для экспериментов. Мы взяли самый легкий из наших проектов и попытались запустить на сервере максимальное количество эмуляторов. Результаты оказались не самыми утешительными.

Посмотрите на график общей нагрузки системы (Load — интегральная метрика, отражающая загрузку Linux-систем по CPU, RAM, Input/Output и тому подобное):



Видно, что когда мы запускаем тесты в 48 потоков (то есть выдавая по два потока на один эмулятор), мы приближаемся к загрузке 80–90%. Проблема в том, что это самый легкий проект, у которого нет ни дополнительной сборки, ни тяжелой нагрузки эмуляторов. Когда мы начали запускать аналогичные тесты на проектах вроде Kaspersky Internet Security, цифры улетели гораздо выше.

При этом от наших эмуляторов требуется минимальное быстродействие. Связано это с тем, что у наших продуктов есть собственные внутренние SLA. Возьмем тот же Kaspersky Internet Security. После его установки первая кнопка, которая ждет пользователя, — Scan. Чтобы пользователь не сидел минутами и не ждал, когда появится первая информация о проблемах, сканирование должно отработать быстро. Предположим, оно должно пройти за 45 секунд. Будет обидно, если мы сделаем автотест, который зафейлится только из-за того, что эмулятор очень медленно отображает интерфейс устройства (и сканирование занимает больше времени вовсе не из-за того, что продукт стал медленнее, а из-за того, что слишком много всего запущено на данной машине).

Таким образом, серверы показали себя не очень. Хорошо, что мы их не покупали, а взяли для экспериментов у наших админов, за что им большое спасибо.
Во-первых, мы отрисовываем все на CPU, создавая дополнительную нагрузку. Во-вторых, есть проблема сложной виртуализации. Эмулятор — это тоже виртуальная машина. Получается, что на железе мы запускаем ОС, в ней эмулятор, внутри эмулятора — приложение. А если мы покупаем не сервер, а виртуальную машину, получаем еще один уровень вложенности виртуализации. Это значительный оверхед по ресурсам, уходящим на поддержку вложенной виртуализации. В итоге все работает очень медленно.

Тем, как работают серверы, мы остались недовольны. Насколько я помню, в среднем на нормальную работу одного эмулятора нам требовалось порядка 6 ядер CPU. Из них три ядра уходило на рендеринг, даже притом что мы понизили разрешение эмулятора и поубивали все анимации. Это крайне дорого.

Почему десктопы


В начале я уже проспойлерил, что в итоге мы накупили десктопов. Оказалось удобнее перенести задачу рендеринга интерфейса на GPU. Так мы экономим три ядра за счет примерно 400 МБ Video RAM. Иными словами, мы можем купить GPU на 4 ГБ, взять CPU 12 ядер hyperthread (то есть 24 виртуальных ядра) и запустить на этом восемь эмуляторов.

В нашем случае конфигурация была следующая:



Мы использовали CPU AMD и GPU Radeon. Почему не NVidia? Карточку NVidia действительно было купить проще, и мы пытались на ней сделать то же самое, но у нас ничего не получилось. По непонятной нам причине, из-за багов самих эмуляторов или других компонент, мы не смогли запустить на NVidia больше 6-7 эмуляторов. В буквальном смысле ты запускаешь 7 эмуляторов — все нормально, стартуешь восьмой — отваливается третий. Стартуешь девятый — отваливается первый. И так далее. Причем неважно, на хосте это происходит или в Docker.

У Radeon-а почему-то таких проблем не было. Возможно, это связано с тем, что у Radeon-а свободные драйверы, которые допиливаются комьюнити (у NVidia драйверы проприетарные). До конца мы эту проблему исследовать не стали — выяснили, что все хорошо работает на Radeon-е, и использовали его.

Про финансы. Все косты — в районе 100 долларов на эмулятор. В случае с сервером выходило 500 долларов на эмулятор. Выгода очевидна.

Как здесь появился Docker


Раньше у нас было два десктопа, и это было несложно. Но потом их стало 8, а затем 10. Нужно распространять по ним эмуляторы и запускать их, запускать сами тесты. И мы подумали, а почему бы не использовать Docker?

Казалось бы, Docker — это еще один уровень абстракции, усложняющий прямой доступ к железу. Но был один нюанс. Мы смогли пробросить видеокарту из хостовой машины в docker, где запускали эмуляторы. Таким образом, рендеринг все еще выполняется на хостовой видеокарте напрямую без оверхеда по ресурсам. Технически мы для этого просто подмонтировали файл /dev/dri в Linux, и это сработало.

Docker позволил нам решить проблему не только запуска эмуляторов, но и их обновления. В системе Android постоянно прилетают мажорные и минорные обновления, патчи безопасности. Эмуляторы (как и системные приложения внутри) нужно обновлять. Благодаря Docker нам не приходится ручками перебрасывать img-файлы с одного десктопа на другой.

Мы, по сути, собрали пайплайн, запаковали в него готовый эмулятор с сохранением памяти (это не снапшот для подъема на горячую, а просто хранение во внутренней памяти эмулятора). У нас получился файл img, файл памяти, файл настроек и еще парочка файлов. Все это пишется в Docker registry. А дальше при запуске тестов машина сама скачивает эмулятор нужной версии с обновлениями, автоматически его запускает и начинает с ним работать. Это удобно.

Нам нужно запускать не только эмулятор. Стандартный набор тестов у нас выглядит как эмулятор плюс Appium, плюс собственно контейнер с тестами (без Appium и отдельного контейнера с тестами, если речь идет про интеграционные тесты). Эти три контейнера мы объединяем в одну сеть и запускаем композицией с помощью docker compose.



Благодаря ADB контейнеры прекрасно видят друг друга в одном сетевом пространстве. Они запускаются все вместе, проходят и переливают нужный файл по окончании прогона обратно на хост. Docker compose закрывается, Network и Volume очищаются — это делает сам Docker. Все возвращается к исходному состоянию. Это получилось очень удобно.

Поскольку Docker позволяет монтировать папки, все результаты автотестов и прочие артефакты сразу складываются на удаленную шару в папку, название которой соответствует Build Number.

Стоит еще сделать одну оговорку про оркестрацию. У нас установлено 15 десктопов, на каждом из них заведено 8 агентов. И тесты отправляются не на десктопы, а на агенты.

Трудности


Железо — самая холиварная тема. У всех компаний различные железные конфигурации, которые получаются из-за различных требований к тестам, их скорости и их профилю. И у этой потрясающей схемы с десктопами есть определенные трудности, на которые нам указали наши коллеги-администраторы. Как ни странно, они оказались правы, потому что:



За потребительским железом действительно надо следить гораздо пристальнее, чем за серверами.

В первую очередь у нас начали дохнуть SSD. Оказывается, если вы запускаете тесты, которые как сумасшедшие долбят в I/O (а эмуляторы очень активно это делают) день за днем, месяцев через девять SSD начинает умирать. А кроме того, через небольшое время у нас помирали порты на кулерах. HDD при запуске большого количества эмуляторов не справляются — по три минуты сидишь и ждешь, пока эмулятор встанет. И это только те проблемы, с которыми мы поняли, что делать.

Были еще проблемы, с которыми мы до сих пор не понимаем, как бороться. Например, машина может просто встать и все, отказываясь даже перезагружаться по sudo reboot. Kernel panic мы наловили больше, чем когда-либо. Сеть регулярно отваливается. Все это результат того, что мы накупили потребительских десктопов и сделали из них инфраструктуру production-уровня. На ней каждый день будет запускаться по несколько тысяч тест-поинтов, каждый из которых поднимает собственный эмулятор, что-то пишет в него, а затем убивает.

Но на самом деле все это звучит страшнее, чем есть на самом деле. Проблемы, безусловно, есть. Их нужно решать. Но понятно, что они не настолько значимы, чтобы мы отказались от всей схемы в пользу серверов. В сумме нам получилось запустить очень большое количество потоков. Поэтому мы заказали новую конфигурацию — на обновленных CPU, SSD с увеличенной устойчивостью к перезаписи. И снова взяли GPU Radeon (поскольку в прошлый раз получилось неплохо).



Что дальше


Как вы уже поняли, отказываться от этой инфраструктуры мы не собираемся. Поэтому мы набрали запасных частей. Мы уже знаем, что SSD может умереть, поэтому накупили больше накопителей. К счастью, мы работаем в Docker, поэтому заменить SSD на машине не представляет вообще никаких проблем. Раскатил систему и настройки, а дальше в Docker все запустится само.

Мы все еще решаем проблему I/O — эмулятор его грузит. И я уже молчу о том, что один эмулятор в сохраненном виде весит около 18 ГБ (а на одной машине их 15–20 штук — по одному на каждый мажор, еще и в разных конфигурациях). С этим мы еще будем разбираться.

И будем закупать новое оборудование — экспериментировать и работать дальше. Если хотите присоединиться к этим экспериментам — поднять эмулятор в Docker или попробовать подсунуть туда карточку NVidia, приходите к нам на позицию Mobile Software Development Engineer in Test. Будем вместе писать автотесты и развивать под них эту инфраструктуру. У нас много планов по тестированию той же схемы на Mac M1. А еще было бы неплохо попробовать запустить тесты в эмуляторах на серверах с GPU. Возможно, это позволит повысить производительность.

Кстати, если вы хотите поучаствовать в наших экспериментах с позиции админа, DevOps-ам мы тоже рады.

Оказалось, тема десктопов затронула не только нашу команду — этот опыт полезен не только нам. Недавно к нам пришли коллеги из KasperskyOS и очень интересовались тем, как мы запускаем тесты на виртуалках, отрисовывая графику через видеокарты. Чем смогли — помогли. В итоге, мне кажется, мы сделали правильный шаг не только с точки зрения компании, но и с точки зрения индустрии мобильного тестирования в целом.

Комментарии (3)


  1. 0Bannon
    25.08.2023 15:14

    Очень интересно. Спасибо.


  1. mpa4b
    25.08.2023 15:14

    Что такое "порты на кулерах" ?

    А ну и да, память/мамки-то с ECC хоть брали, раз уж AMD?


  1. Coderx64
    25.08.2023 15:14

     Были еще проблемы, с которыми мы до сих пор не понимаем, как бороться.  Например, машина может просто встать и все, отказываясь даже  перезагружаться по sudo reboot. Kernel panic мы наловили больше, чем  когда-либо.  

    ryzen 2700 <- this. все что на микроархитектуре старее zen2, имеет хардварные баги - приводящие к рандомным зависаниям и тд.