Pull to refresh
220.78
Конференции Олега Бунина (Онтико)
Конференции Олега Бунина

Писать нативные автотесты для iOS сложно? Это пока вы их запускать не начнёте

Reading time13 min
Views1.8K

Писать нативные тесты под iOS — это не очень простая задача. Но если вы всё-таки написали эти тесты, то оказывается, что их нужно ещё и запускать. Тут тоже непросто. Сегодня про это и поговорим.

Поможет нам в этом Тимофей Солонин. Он поддерживает IOS-инфраструктуру в Авито. Сегодня расскажет про технологию Emcee. С её помощью можно параллельно запускать нативные IOS-тесты на большой ферме Apple-железа.

Как происходит релиз в Авито

Мобильные приложения Авито релизятся раз в «неделю», но эти «недели» между собой не равны. Одна «неделя» — это полноценный регресс, то есть кроме прогона автотестов, команды могут проверить какие-то фичи руками. Вторая «неделя» совсем другая: никакого ручного регресса не происходит, после прогона всего сьюта автотестов на всех поддерживаемых OS  приложение катится в production.

Но давайте представим обратную ситуацию, что за неделю зарелизиться не получится.

Релиз без тестов

Как может выглядеть релиз без тестов?

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

Спустя какое-то время, приложение получится заревьювить и покатить пользователям в production. Но потом вдруг окажется, что приложение в каком-нибудь edge case крашится. Придётся снова начинать разбираться в баге, чинить его, заново выкатывать приложение в store и проходить ревью.

Как итог, релиз без тестов за неделю абсолютно точно не покатится. Команда Авито потому так и не делает.

Анализ импакта за неделю

Тимофей подобрал 3 объективные метрики импакта за неделю в Авито:

  • 19к Swift sloc;

  • 100 Jira-задач;

  • В среднем 50 коммитеров каждую неделю.

Со всем этим импактом нужны тесты для того, чтобы убедиться, что приложение работает.

Какие тесты пишут в Авито

— Unit-тесты

Это самые понятные тесты. В Авито, в среднем, каждый тест проходит меньше, чем за секунду. Их легко написать и они обычно супер-стабильные. Но есть одна проблема — баги сквозь пальцы Unit-тестов очень легко проходят. Даже имея кучу таких тестов, пользователи приложения Авито всё равно сталкиваются с проблемами. Поэтому в Авито не полагается только на Unit-тесты, у них есть ещё in-house разработка, которая помогает писать тесты других типов.

— Component UI тесты

Эти тесты не такие быстрые как Unit: в среднем они проходят за 7 секунд. Но компонентные UI-тесты помогают хорошо протестировать UI-логику. Они не ходят в сеть и по написанию похожи на Unit-тесты только на UI. Также они помогают проверить state экранов, потому что любое iOS приложение во многом состоит из вёрстки какого-то state экранов и view’шек. Минус компонентных UI-тестов в том, что их сложнее писать, чем Unit-тесты, и идут они чуть подольше.

— E2E UI тесты

Такие тесты проходят в среднем за 84 секунды, но хорошо всё проверяют. Выглядит это следующим образом. Сначала поднимается тестовое окружение с бэкендом. Создаётся пользователь, через которого происходит логин на телефон. Потом проходится длинный flow, тыкается куча кнопок, и отправляются запросы в бэкенд. Это помогает убедиться, что приложение работает так, как было задумано. Но поскольку у E2E-тестов огромное количество точек отказов, они флакуют. Это их главная проблема: они очень нестабильные. А ещё E2E тесты долго писать, потому что нужно поддерживать тестовую инфраструктуру.

Пирамида здорового приложения

Команда разработки Авито комбинируют каждый из этих видов тестов для того, чтобы получилась нормальная пирамида тестирования:

В результате релиз выглядит следующим образом:

Никто, конечно, ни за кем не бегает. Просто робот присылает сообщение о том, что релиз начался, столько-то тестов прошло, а столько-то упало. Если какая-то команда хочет проверить свои красные тесты руками, то они делают это самостоятельно. Но многие команды ничего руками не проверяют, а просто полагаются на свои зелёные тесты.

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

В Авито используется 600 штук E2E UI-тестов, которые в среднем исполняются по 84 секунды. Получается 14 часов. Компонентных тестов — 2000 штук. Каждый из них исполняется по 7 секунд. Итого: ещё 4 часа. И наконец, на все Unit-тесты уходит 1 час. В сумме получается 19 часов на все тесты. И это только если запускать их на одной версии OS, тогда как мы поддерживаем последние 3, и перед релизами и ночью проверяем тесты на всех поддерживаемых ОС.

Кроме того компонентные и Unit-тесты запускаются на каждый pull-request, которых примерно 100 штук в неделю. Но в рамках каждого pull-request тесты могут по несколько раз перезапускаться, потому что разработчики пушат  дополнительный код.

Надо запускать тесты параллельно!

Конечно же никто не ждёт по 5 часов на каждом pull-request и 19 часов во время регресса. У инженеров Apple тут всё схвачено. Они предоставляют утилиту xcodebuild. У неё есть команда:

xcodebuild test-without-building -parallel-testing-enabled YES

В ней можно прописать, чтобы тесты запускались параллельно.

Но сколько на одной машине можно запустить параллельно тестов? Какие здесь ограничивающие факторы? Всё максимально понятно: CPU + RAM. Иначе говоря, ограничивающие факторы — это то, насколько у нас мощный процессор, сколько у него ядер, и сколько оперативной памяти у машины .

Давайте посмотрим на линейку железа от Apple с точки зрения того, сколько на каждом из них можно запустить тестов.

Первый вариант, который очень нравится Тимофею — это M1 mini. В нём целых 8 ядер, 16 GB оперативки и стоит это максимально демократично ($1100). На таком mini можно запустить по замерам команды Авито 5 симуляторов, каждый из которых будет стоить $220. При этом цифра 5 симуляторов может варьироваться в зависимости от того, какую нагрузку дают тесты.

У Apple есть более мощная железка — Mac Studio с M1 Ultra. Здесь больше ядер, целых 20 штук, и 128 GB оперативки. Стоит это, конечно, подороже ($4800), но там уже можно запустить 15 симуляторов, каждый из которых будет стоить $319.

Самое лучшее и мощное железо от Apple — это Mac Pro на колёсиках со старейшим процессором Xeon W. Стоит он всего лишь «жалкие» $50000. На такой машине вы не сможете запустить сильно больше симуляторов, чем на M1 Ultra, зато цена симулятора сразу в космос улетает ($3333).

Поэтому у нас только 2 варианта — либо M1 mini, либо Mac Studio.

Где можно запустить больше тестов 

Если посмотреть на цифры, сколько симуляторов можно параллельно запустить, то выглядит всё не слишком радужно. Представьте, что вы написали 100-3000 тестов. Получается, что максимум вы их можете разгребать на 15 симуляторов — такая себе перспектива. 

Но что, если бы вы могли взять свой недорогой M1 mini и запустить тесты параллельно не только на нём и даже не на трёх железках, а на всей ферме железа Apple? Для этого есть Emcee.

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

Как работает Emcee под капотом

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

Первое, что происходит, — запускается сама очередь. Она поднимает воркеров, одного за другим. Воркерами могут быть самые разные машины с MacOS на борту. Дальше очередь берёт тесты и делит их на небольшие кусочки, которые каждый воркер сможет взять и начать исполнять параллельно. Такие кусочки в Emcee называются bucket’ами. В одном bucket’е может быть несколько тестов. Как только распределение завершено, воркеры начинают исполнять тесты.

Как возвращаются результаты прогона тестов

Представим, что на воркере A bucket уже успел исполниться, то есть пришли тесты. Это значит, что воркер A может себе взять ещё один bucket и начать его тоже параллельно исполнять. И так на всей ферме: где-то тесты проходят, где-то падают. Потом очередь агрегирует в себя результаты выполнения этих тестов.

Дальше очередь должна вернуть результаты прогона тестов. В каком формате их возвращать? Есть несколько вариантов.

Простой вариант

Самый простой вариант — это JUnit xml. Это максимально примитивный формат: xml-файлик, в котором говорится, какой тест прошёл, а какой упал и примерная причина падения этого теста. Такой файлик в Авито используется для интеграции с TeamCity.  CI отдаёт в TeamCity JUnit xml файл, а TeamCity уже рисует UI, на котором видны результаты тестов.

Подробный вариант

Более интересный вариант, нежели JUnit xml — Result Bundle файлы. Последняя версия Emcee может собирать их с прогонов всех машин и склеивать в один единый файл, как если бы все тесты прошли на одной машине. Result Bundle файлы содержат в себе всю информацию о том, как тест исполнялся и какие были логи. Иначе говоря, всё, что только можно придумать про тест, содержится в Result Bundle файле.

DYI

Если по какой-то причине вам не подходит ни JUnit xml, ни Result Bundle, в Emcee есть решение, для тех, кто все любит делать самостоятельно: бинарные плагины.

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

Что умеет воркер

Расскажу подробнее, что происходит внутри воркера и внутри очереди.

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

Однако, поднять симуляторы — это не всё, что требуется для того, чтобы запустить тесты. Нужно ещё эти симуляторы настроить. Представьте себе, что вы купили новый iPhone, в нём вылезает куча полезных подсказок. Например, как печатать на клавиатуре. Для людей это полезно, но автотесты — от таких штук падают. Emcee умеет настраивать state симулятора так, чтобы симуляторы находились в стабильном состоянии и не валили просто так тесты.

После того как симуляторы настроены, нужно прогнать тесты. Тут тоже всё непросто, потому что все утилиты Apple имеют свойство зависать по непонятным причинам. Воркер отслеживает, чем закончилось выполнение каждой утилиты, которую он вызывал, отключает её, перезапускает, если что, и мейнтейнит весь этот сложнейший state. Вам же про это думать не нужно.

Что умеет очередь

Главная задача очереди — это взять тесты, распилить их на маленькие кусочки и раздать воркерам, а потом собрать результаты.

Представьте, что тесты у вас исполняются на одной и на другой машине. Всё идёт замечательно. Но потом появляется неказистый воркер, который пытается исполнить прилетевший ему тест и у него не получается. Причин, почему воркеры фейлят тесты, может быть много. Но самая очевидная — на машине закончилось место на жёстком диске. Тогда на машине начнут падать все процессы подряд. Но это не значит, что с нашим тестом что-то не так. Поэтому очередь умеет перезапускать тесты на других машинах. Если очередь видит, что тест упал на одной машине, она его перекидывает на другую и там запускает этот тест. Если он проходит успешно, то очередь возвращает результат о том, что тест прошел успешно.

Ещё очередь умеет запускать воркеров. Этот процесс происходит по ssh. Очередь свой бинарник клонирует в воркера, дальше setup’ит ему launchd. Потом запускает процесс воркера через launchd для того, чтобы процесс воркера был максимально отвязан от всех других процессов в системе.

Затем очередь бьёт тесты на части. Когда это сделано, нужно раздать эти тесты воркерам. Если какие-то тесты упали, очередь умеет их перезапускать на других воркерах или на том же самом. Этот процесс можно настроить.

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

Как понять что происходит внутри

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

Метрики и Grafana

В Grafana есть графики по каждому воркеру, который гоняет тесты и метрики для этих воркеров. Они летят в graphite или statsd. Воркер может отключиться если, например, на нём будет kernel panic, то есть, когда машина сама перезагружается. Можно ещё посмотреть, насколько быстро очередь разгребается, сколько bucket’ов процессится и другие полезные метрики.

Логи и Kibana

Кроме метрик в Emcee есть ещё distributed logs. Например, на какой-то машине может запускаться xcodebuild, на другой — state-симулятор настраивается, а на третьей — по ssh происходит что-то. Мало приятного в том, чтобы ходить и грепать эти логи по машинам. Поэтому в Emcee есть интеграция с Kibana. В Kibana можно прямо посмотреть подробные логи каждого из этих процессов.

Например, здесь Тимофей хотел посмотреть, сколько тестов у него в Авито запустилось за последнюю неделю:

Для этого он просто вбил месседж, который отправляется при старте теста. Видно, запустилось примерно 1,5 млн тестов за последнюю неделю.

О железе

Теперь стоит рассказать про железо, на котором всё происходит.

Основная рабочая лошадка Авито — это 80 Mac mini. Такие машины стоят в дата-центре. Это очень грустное зрелище, потому что красивые машины заперты в железных коробках. Команду Тимофея это очень печалило, они ведь всё-таки iOS разработчики: им хочется, чтобы их CI был стильный и красивый. Поэтому они купили себе 30 iMac телевизоров, на которых тоже гоняют тесты и билды в их CI.

При этом из их CI они выжимают вообще все соки, какие только можно. 

Например, они пытались поменять термопасту на старых mini. Сразу стоит вас предупредить, что это путь в никуда. Не меняйте термопасту на старых миниках. Там нормальная термопаста, только миники себе поломаете. Но если вы всё-таки любители риска, то есть вариант помазать свои миники жидким металлом. Это по замерам команды Тимофея даёт ускорение Intel-машин на 10-15%. На M1 машинах жидкий металл не используйте, там это не помогает.

Управляем железом через Puppet

Если у вас целая куча всякого железа, прям как в Авито, то им нужно как-то управлять.

Puppet — это такой ssh на стероидах, где можно написать какие-то скрипты и построить граф зависимостей между этими скриптами. Например, можно через brew поставить парсер логов:

brew install xclogparser

Или, как в Авито, раскатывать xcode.sh через Puppet:

install_xcode.sh

Но если у вас меньше 20-30 машин, то проще управлять руками. Подумайте, нужна ли вам вся эта сложность с Puppet. Нет ничего суперсложного в том, чтобы руками по ssh или по vnc поставить xcode.

Демо

Мы узнали про ферму Авито. Теперь вживую посмотрим, насколько просто взять Emcee и собственное железо, и распараллелить на нём запуск тестов.

Первое, нужно убедиться, что на всех наших машинах включён ssh. Для этого надо пойти в System Preferences, нажать кнопку Sharing и поставить галочку на Remote Login. После этого ssh на машинах должен включиться.

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

Второй requirement для того, чтобы запускать тесты через Emcee — поставленный xcode. Чтобы проверить, воспользуемся командой xcode-select -p. Чтобы проверить, что xcode точно настроен, можно сделать xcodebuild -runFirstLaunch. В таком случае xcode точно должен работать.

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

Для начала клонируем репозиторий Emcee:

git clone https://github.com/avito-tech/Emcee.git

Там же можно найти полную документацию. 

Затем перейдём в папку Emcee/Samples/Emcee Sample. Там лежат три основных вида нативных тестов, которые есть в мире iOS и вообще в принципе Apple-разработки:

  1. Тесты без test host;

  2. Тесты с Host Application;

  3. XCUI тесты. 

В первом тесте если вы перейдете в xcode во вкладку general, то увидите, что Host Application будет None. У второго, соответственно, наоборот — Host Application установлен. А у третьего будет не Host Application, а Target Application.

Что в этих тестах происходит? Они максимально тривиальные: три теста проходят, и один всегда падает для того, чтобы можно было потом посмотреть, как выглядит падение. А ещё в XCUI тестах есть слипы, чтобы можно было насладиться запуском XCUI тестов.

Запустим эти тесты. Сначала создаем проект:

xcodebuild build-for-testing \

	–-project EmceeSample.xcodeproj \ 

	--sdk iphonesimulator \ 

	--scheme AllTests \

	--derive@DataPath derivedData

Теперь подставляем последнюю версию Emcee на ту же машину, где был создан проект: 

curl -L https://github.com/avito-tech/Emcee/releases/download/17.0.0/Emcee -o Emcee && chmod +x Emcee

Какие команды есть у Emcee? Чтобы посмотреть все команды, запустим команду help. Там есть целая куча всего, но мы будем пользоваться runTests. Она позволяет запустить тесты максимально просто, не заморачиваясь вообще с конфигурацией машин и очередей. Также есть команда runTestsOnRemoteQueue. С её помощью можно настроить всё, что хотите.

Тесты без host

Запустим тесты без host с помощью команды runTests. В ней куча настроек, посмотрим их с помощью аргумента help:

./Emcee runTests –help

А воспользуемся лишь некоторыми из них:

./Emcee runTests \

	--queue ‘ssh://emcee:qwerty@192.168.2.2’ \

	--worker ‘ssh://emcee:qwerty@192.168.2.2?numberOfSimulators=1’ \

	--worker ‘ssh://emcee:qwerty@192.168.2.1?numberOfSimulators=1’ \

	--device ‘iPhone X’ \

	--runtime 15.0 \

	--retries 0 \

	--junit junit_without_host.xml \

	–result-bundle resultBundle_without_host.xcresult \

	--test-attachment-lifetime keepAlways \

	--test-bundle derivedData/Build/Products/Debug-iphonesimulator/EmceeSampleTestsWithoutHost.xctest

В параметре queue нужно указать, где будет разворачиваться очередь. Для этого мы пишем, что она запускается по ssh. Вводим логин — Emcee и пароль — qwerty и указываем адрес машины. Можно также указать доменное имя, если у машин оно есть.

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

В параметре device прописываем на каком устройстве будем запускать тесты. В нашем случае — это iPhone X на runtime’e iOS симулятора 15.0.

Также можно выделить количество перезапусков в параметре retries. Мы сейчас установили 0, потому что знаем, что у нас 3 теста всегда проходят, а 1 падает. Нет смысла ждать перезапуска одного теста, который всегда будем падать.

Следующие параметры ответственны за формат, в котором мы хотим получить результат. Мы одновременно указали junit и result-bundle. Ещё мы указали keepAlways в параметре test-attachment-lifetime чтобы, вся информация, которая только может попасть в Result Bundle, попадала в Result Bundle.

И наконец, говорим, что за тесты мы будем запускать. Для этого указываем путь до тестового bundle, который содержит тесты.

Когда мы всё это запустим, первым делом Emcee пойдёт разворачивать очередь. Этот процесс можно наблюдать в режиме реального времени. Для этого открываем Finder. В домашней директории вашего пользователя появится папка emcee_noindex. Там будет видно, как разворачивается очередь и воркер на каждой машине. Там будут находиться первоначальные логи того, как воркер запускался при помощи launchd. Если что-то пошло не так, то можно в эти логи посмотреть, чтобы увидеть подсказку. Кроме этого, Emcee записывает логи в папку Library/Logs. Там более подробная информация о том, что происходило в очереди. Также можно посмотреть, каким результатом завершились все подпроцессы, которые запускала очередь или воркеры.

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

Тесты с host

Теперь посмотрим как запускать тесты с test host. Мы используем всю ту же команду runTests с теми же настройками. Только добавим параметр app:

--app derivedData/Build/Products/Debug-iphonesimulator/EmceeSample.app \

Там нужно указать путь до приложения, в котором будут запускаться тесты.

После запуска очередь уже не будет разворачиваться, а воркеры — подниматься. Этот процесс работает быстрее, так как тесты исполняются прямо там, где развернулись воркеры и очередь. Остаётся лишь дождаться результата прогона этих тестов.

XCUI тесты

Теперь запустим последний вид тестов — XCUI тесты. В них тоже добавляется ещё один параметр — runner:

--runner derivedData/Build/Products/Debug-iphonesimulator/EmceeSampleUITests-Runner.app \

В этом параметре прописан путь к приложению, которое будет контролировать ваше приложение с тестами.

Вот как это выглядит:

Результат

Посмотрим как выглядит Result Bundle. Но перед этим сначала нужно все Result Bundle файлы склеить в единый bundle. Для этого пишем:

xcrun xcresulttool merge *.xcresult --output-path merged.xcresult

Если открыть этот bundle, то там содержится информация о всех запусках тестов. При этом самая подробная информация по XCUI тестам. Она включает в себя все attachment. Например, скриншоты, которые делались во время теста.

Весь этот Result Bundle можно отобразить в виде Allure Report. Для этого скачаем опенсорсную утилиту:

curl -L https://github.com/eroshenkoam/xcresults/releases/latest/download/xcresults -o xcresults && chmod +x xcresults

Она помогает Result Bundle экспортировать в формат, который может потом отобразить Allure.

И прописываем ещё одну команду, которая экспортирует наш склеенный Result Bundle в Allure Report:

./xcresults export merged.xcresults allure_report

Теперь ставим утилиту Allure через brew и отрисовываем:

allure serve allure_report

Вывод

Что мы узнали?

  1. Что такое нативные iOS-тесты и как они помогают бороться с проблемой Time to Market, то есть с тем как доставлять в продакшен как можно быстрее.

  2. Про технологию Emcee, которая позволяет запускать 250 тысяч тестов каждый день параллельно на ферме Apple-железа.

  3. Как Emcee устроен под капотом и почему написать свой собственный Test Runner может быть не так просто, как кажется на первый взгляд.

  4. Про железо в Авито, на котором все эти тесты запускаются.

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

Tags:
Hubs:
Total votes 2: ↑2 and ↓0+2
Comments0

Articles

Information

Website
www.ontico.ru
Registered
Founded
Employees
11–30 employees
Location
Россия