Многим разработчикам UI-тестов под iOS наверняка знакома проблема времени тестового прогона. В Badoo прогоняется более 1400 end-to-end тестов для iOS-приложений на каждый запуск регрессии. Это более 40 машинных часов тестов, которые проходят за 30 реальных минут.
Николай Абалов из Badoo поделился тем, как удалось ускорить выполнение тестов с 1,5 часов до 30 минут; как распутали тесно связанные тесты и инфраструктуру iOS, перейдя к серверу устройств; как это упростило параллельный запуск тестов и сделало тесты и инфраструктуру проще для поддержки и масштабирования.
Вы узнаете, как легко запускать тесты параллельно с помощью таких инструментов, как fbsimctl, и как разделение тестов и инфраструктуры может упростить принятие, поддержку и масштабирование ваших тестов.
Изначально Николай представил доклад на конференции Heisenbug (можно посмотреть видеозапись), а теперь для Хабра мы сделали текстовую версию доклада. Далее — повествование от первого лица:
Всем привет, сегодня я расскажу про масштабирование тестирования под iOS. Меня зовут Николай, я в Badoo занимаюсь в основном инфраструктурой iOS. До этого три года работал в компании 2ГИС, занимался разработкой и автоматизацией, в частности, написал Winium.Mobile — реализацию WebDriver для Windows Phone. Меня взяли в Badoo работать над автоматизацией Windows Phone, но через некоторое время бизнес принял решение приостановить развитие этой платформы. И мне предложили интересные задачи по автоматизации iOS, про это я сегодня и расскажу.
О чем мы поговорим? План таков:
- Неформальная постановка проблемы, введение в используемые инструменты: как и почему.
- Параллельное тестирование на iOS и как оно развивалось (в частности, по истории нашей компании, так как мы начали заниматься им еще в 2015-м).
- Device server — это основная часть доклада. Наша новая модель распараллеливания тестов.
- Результаты, которых мы достигли с помощью сервера.
- Если у вас нет 1500 тестов, то device server вам может быть особо не нужен, но из него все равно можно вытащить интересные штуки, и речь пойдет о них. Их можно применить, если у вас 10-25 тестов, и это все равно даст либо ускорение, либо дополнительную стабильность.
- И, наконец, подведение итогов.
Инструменты
Первое — это немного о том, кто чем пользуется. Наш стек немного нестандартный, потому что мы одновременно используем и Calabash, и WebDriverAgent (что дает нам скорость и бэкдоры Calabash при автоматизации нашего приложения и в тоже время полный доступ к системе и другим приложениям через WebDriverAgent). WebDriverAgent — это реализация WebDriver для iOS от Facebook, которая используется внутри Appium. А Calabash — встраиваемый сервер для автоматизации. Сами тесты мы пишем в человекочитаемом виде с помощью Cucumber. То есть у нас в компании псевдо-BDD. И из-за того, что мы использовали Cucumber и Calabash, мы унаследовали Ruby, весь код написан на нем. Кода очень много, и приходится продолжать писать на Ruby. Чтобы запускать тесты параллельно, мы используем parallel_cucumber, инструмент, написанный одним из моих коллег в Badoo.
Начнём с того, что у нас было. Когда я начал готовить доклад, имелось 1200 тестов. К моменту завершения их стало 1300. Пока я сюда доехал, тестов стало уже 1400. Это end-to-end тесты, а не unit-тесты и не интеграционные тесты. Они составляют 35-40 часов машинного времени на одном симуляторе. Проходили они раньше за полтора часа. Расскажу, как они стали проходить за 30 минут.
У нас в компании есть рабочий процесс с ветками, ревью и запуском тестов на эти ветки. Разработчики создают примерно 10 запросов в основной репозиторий нашего приложения. Но в нем есть также компоненты, которые шарятся с другими приложениями, поэтому иногда бывает и больше десяти. В итоге 30 тестовых запусков в день, как минимум. Поскольку разработчики пушат, затем понимают, что запушили с багами, перезаливают, и на все это запускается полная регрессия, просто потому что мы можем ее запускать. На той же инфраструктуре мы запускаем дополнительные проекты, такие как Liveshot, который снимает скриншоты приложения в основных пользовательских сценариях на всех языках, чтобы переводчики могли верифицировать корректность перевода, помещается ли он в экран и так далее. В результате выходит около полутора тысяч часов машинного времени на данный момент.
В первую очередь мы хотим, чтобы разработчики и тестировщики доверяли автоматизации и полагались на неё для уменьшения ручной регрессии. Чтобы это произошло, необходимо добиться от автоматизации быстрой и, главное, стабильной и надежной работы. Если тесты проходят за полтора часа, разработчик устанет ждать результатов, он начнет делать другую задачку, его фокус переключится. А когда какие-то тесты упадут, он будет очень сильно не рад, что приходится возвращаться обратно, переключать внимание и что-то чинить. Если же тесты ненадежные, то со временем люди их начинают воспринимать только как преграду. Они постоянно падают, хотя багов в коде нет. Это Flaky-тесты, какая-то помеха. Соответственно, эти два пункта были раскрыты в настоящие требования:
- Тесты должны проходить за 30 минут или быстрее.
- Они должны быть стабильными.
- Они должны быть масштабируемыми, чтобы мы могли, добавив еще сотню тестов, укладываться в полчаса.
- Инфраструктура должна легко поддерживаться и разрабатываться.
- На симуляторах и физических девайсах все должно запускаться одинаково.
В основном мы гоняем тесты на симуляторах, а не на физических девайсах, потому что это быстрее, стабильнее и проще. Физические девайсы используются только для тестов, которые реально этого требуют. Например, камера, push-нотификации и подобное.
Как же удовлетворить эти требования и сделать всё хорошо? Ответ очень прост: мы удаляем две трети тестов! Это решение укладывается в 30 минут (потому что тестов осталась только треть), легко масштабируется (можно удалить больше тестов), и повышает надежность (потому что первым делом мы удаляем самые ненадежные тесты). На этом у меня все. Вопросы?
Но если серьезно, в каждой шутке есть доля правды. Если у вас очень много тестов, то надо пересмотреть их и понять, какие приносят реальную пользу. У нас же была другая задача, поэтому мы решили посмотреть, что можно сделать.
Первый подход — фильтрация тестов на основе coverage или компонентов. То есть подбирать соответствующие тесты на основе изменений файлов в приложении. Про это я рассказывать не буду, но это одна из задач, которую мы решаем в данный момент.
Другой вариант — это ускорение и стабилизация самих тестов. Вы берете конкретный тест, смотрите, какие шаги занимают в нем больше всего времени и можно ли их как-то оптимизировать. Если какие-то из них очень часто нестабильны, вы их правите, потому что это сокращает перезапуски тестов, и все проходит быстрее.
И, наконец, совершенно другая задача — параллелизация тестов, распределение их на большое количество симуляторов и обеспечение масштабируемой и стабильной инфраструктуры, чтобы было куда параллелизовать.
В этой статье поговорим в основном про последние два пункта и в конце, в tips & tricks, затронем пункт про скорость и стабилизацию.
Параллельное тестирование под iOS
Начнем с истории параллельного тестирования под iOS в целом и в Badoo в частности. Для начала простая арифметика, тут, правда, в формуле ошибка есть, если размерности сопоставить:
Было 1300 тестов на один симулятор, получается 40 часов. Тут приходит Сатиш, мой руководитель, и говорит, что ему нужно полчаса. Надо что-то придумывать. В формуле появляется X: сколько симуляторов запустить, чтобы за полчаса все прошло. Ответ – 80 симуляторов. И сразу возникает вопрос, куда эти 80 симуляторов засунуть, ведь они никуда не влезают.
Есть несколько опций: можно пойти в облака типа SauceLabs, Xamarin или AWS Device Farm. А можно все придумать у себя и сделать хорошо. Учитывая, что эта статья существует, мы сделали все у себя хорошо. Мы решили так, потому что облако с таким масштабом обойдется достаточно дорого, а также была ситуация, когда вышел iOS 10 и Appium почти месяц выпускал для него поддержку. Это значит, что в SauceLabs мы месяц не смогли бы автоматически тестировать iOS 10, что нас никак не устраивало. Кроме того, все облака закрытые, и вы не сможете на них повлиять.
Итак, мы решили все делать in-house. Начали где-то в 2015 году, тогда Xcode не умел запускать более одного симулятора. Как выяснилось, он не может запускать более одного симулятора под одним пользователем на одной машине. Если у вас много пользователей, то симуляторов можно запускать сколько угодно. Мой коллега Тим Баверсток придумал модель, на которой мы жили достаточно долго.
Есть агент (TeamCity, Jenkins Node и подобное), на нем запускается parallel_cucumber, который просто идет на удаленные машины по ssh. На картинке изображены две машины под два пользователя. Все нужные файлы типа тестов копируются и запускаются на удаленных машинах по ssh. А тесты уже запускают симулятор локально на текущем рабочем столе. Чтобы это все работало, на каждую машину нужно было предварительно сходить, создать, например, 5 пользователей, если вы хотите 5 симуляторов, сделать одному пользователю автоматический логин, открытие screensharing для остальных, чтобы у них всегда был рабочий стол. И настроить ssh-демон, чтобы у него был доступ к процессам на рабочем столе. Таким нехитрым образом мы начали запускать тесты параллельно. Но на этой картинке есть несколько проблем. Во-первых, тесты управляют симулятором, они находятся там же, где и сам симулятор. То есть их надо запускать всегда на маках, они съедают ресурсы, которые могли быть потрачены на запуск симуляторов. В итоге у вас меньше симуляторов на машине и они дороже стоят. Другой момент заключается в том, что надо сходить на каждую машину, настроить пользователей. А затем вы просто уткнетесь в глобальный ulimit. Если есть пять пользователей и они поднимают много процессов, то в какой-то момент в системе закончатся дескрипторы. Достигнув лимита, тесты начнут падать при попытках открыть новые файлы и запустить новые процессы.
В 2016-2017 годах мы решили перейти к немного другой модели. Посмотрели доклад Лоренса Ломакса из Facebook — они представили fbsimctl, и частично рассказали, как работает инфраструктура в Facebook. Был еще доклад Виктора Короневича про эту модель. Картинка не сильно отличается от предыдущей — мы просто избавились от пользователей, но это большой шаг вперед, поскольку теперь есть только один рабочий стол, меньше запускается процессов, симуляторы стали дешевле. На этой картинке три симулятора, а не два, так как освободились ресурсы для запуска дополнительного. С этой моделью мы жили очень долго, до середины октября 2017 года, когда мы начали переходить на наш сервер удаленных устройств.
Так выглядело железо. Слева ящик с макбуками. Почему мы все тесты запускали на них — это отдельная большая история. Запускать тесты на макбуках, которые мы вставили в железный ящик, было не самой хорошей идеей, потому что где-то к обеду они начинали перегреваться, так как из них плохо уходит тепло, когда они так лежат на поверхности. Тесты становились нестабильными, особенно часто стали падать симуляторы при загрузке.
Решили мы это просто: поставили ноутбуки «палаточками», площадь обдува увеличилась и неожиданно повысилась стабильность инфраструктуры.
Так что иногда надо не софтом заниматься, а ходить поворачивать ноутбуки.
Но если вы посмотрите на эту картинку, тут какое-то месиво из проводов, переходников и вообще жесть. Это железная часть, и она еще хорошая была. В софте же творилось полнейшее переплетение тестов с инфраструктурой, и дальше так жить было невозможно.
Мы определили следующие проблемы:
- То, что тесты были тесно связаны с инфраструктурой, запускали симуляторы и управляли всем их жизненным циклом.
- Это приводило к сложности масштабирования, поскольку добавление новой ноды подразумевало ее настройку и для тестов, и для запуска симуляторов. Например, если вы хотели обновить Xcode, приходилось добавлять workaround прямо в тесты, потому что они гоняются на разных версиях Xcode. Появляются какие-то кучи if для запуска симулятора.
- Тесты привязаны к машине, где находится симулятор, и это обходится в копеечку, так как их приходится запускать на маках вместо *nix, которые дешевле.
- И всегда было очень просто покопаться внутри симулятора. В некоторых тестах мы ходили в файловую систему симулятора, удаляли там какие-то файлы или изменяли их, и все было хорошо, пока это не делалось тремя разными способами в трех различных тестах, а потом неожиданно четвертый начинал падать, если ему не повезло запуститься после тех трех.
- И последний момент — ресурсы никак не шарились. Было, например, четыре TeamCity agent, к каждому было подключено пять машин, и запускать тесты можно было только на своих пяти машинах. Не было какой-то централизованной системы управления ресурсами, из-за этого, когда приходит всего одна задача, она шла на пяти машинах, а все остальные 15 простаивали. Из-за этого билды шли очень долго.
Новая модель
Мы решили перейти к красивой новой модели.
Убрали все тесты на одну машину, где TeamCity agent. Эта машина теперь может находиться на *nix или даже на Windows, если вам так захотелось. Они будут общаться по HTTP с какой-то вещью, которую мы назовем device server. Все симуляторы и физические девайсы будут находиться где-то там, а тесты будут запускаться здесь, запрашивать девайс по HTTP и дальше с ним работать. Схема очень простая, всего два элемента на диаграмме.
В реальности, конечно, позади сервера остался ssh и прочее. Но теперь это никого не волнует, потому что ребята, пишущие тесты, в этой схеме находятся наверху, и у них есть какой-то определенный интерфейс для работы с локальным или удаленным симулятором, поэтому у них все хорошо. А я теперь работаю ниже, и у меня все как было. Приходится с этим жить.
Что это дает?
- Во-первых, разделение ответственности. В какой-то момент при автоматизации тестирования надо рассматривать ее как обычную разработку. В ней работают те же принципы и подходы, которые используют разработчики.
- Получился строго определенный интерфейс: напрямую делать что-то с симулятором нельзя, для этого надо открыть тикет в device server, а мы придумаем, как это сделать оптимально, не ломая другие тесты.
- Тестовая среда стала дешевле, потому что мы ее подняли в *nix, которые значительно дешевле маков в обслуживании.
- И появился шеринг ресурсов, потому что есть единая прослойка, с которой все общаются, она может спланировать распределение машин, находящихся за ней, т.е. разделение ресурсов между агентами.
Выше изображено, как было раньше. Слева условные единицы времени, допустим, десятки минут. Есть два агента, к каждому подключено 7 симуляторов, в момент времени 0 приходит билд и занимает 40 минут. Через 20 минут приходит еще один, и занимает то же время. Все вроде замечательно. Но и там, и там есть серые квадратики. Они означают, что мы потеряли деньги, так как не использовали имевшиеся ресурсы.
Можно сделать так: приходит первый билд и видит все свободные симуляторы, распределяется, и тесты ускоряются в два раза. Для этого ничего не надо было делать. В реальности такое часто происходит, потому что разработчики редко пушат свои бранчи в одну и ту же минуту. Хотя иногда такое происходит, и начинаются «шашечки», «пирамиды» и тому подобное. Тем не менее, в большинстве случаев бесплатное ускорение в два раза можно получить, просто поставив централизованную систему управления всеми ресурсами.
Другие причины перейти к этому:
- Black boxing, то есть теперь device server — это черный ящик. Когда вы пишете тесты, вы думаете только о тестах и считаете, что этот черный ящик всегда будет работать. Если не работает, вы просто идете и стучите тому, кто его должен делать, то есть мне. А мне приходится его чинить. Не только мне на самом деле, всей инфраструктурой занимается несколько человек.
- Нельзя никак испортить внутренности симулятора.
- Не надо ставить миллион утилит на машину, чтобы все запускалось — вы просто ставите одну утилиту, скрывающую всю работу в device server.
- Стало легче обновлять инфраструктуру, о чем мы поговорим где-то в конце.
Резонный вопрос: почему не Selenium Grid? Во-первых, у нас существовало много legacy-кода, 1500 тестов, 130 тысяч строк кода для разных платформ. И все это управлялось parallel_cucumber, который создавал жизненный цикл симулятора вне теста. То есть был специальная система, которая загружала симулятор, ждала его полной готовности и отдавала его в тест. Чтобы все не переписывать, мы решили попробовать не использовать Selenium Grid.
Еще у нас много нестандартных действий, и мы очень редко используем WebDriver. Основная часть тестов на Calabash, а WebDriver только вспомогательно. То есть мы не используем Selenium в большинстве случаев.
И, конечно, мы хотели, чтобы все было гибко, легко прототипировалось. Потому что весь проект начался просто с идеи, которую решили проверить, реализовали за месяц, все завелось, и она стала основным решением у нас в компании. Кстати, вначале мы написали на Ruby, а потом переписали device server на Kotlin. Тесты оказались на Ruby, а сервер на Kotlin.
Device server
Теперь подробнее про сам device server, как он работает. Когда мы только начали исследовать этот вопрос, у нас использовались следующие инструменты:
- xcrun simctl и fbsimctl — утилиты командной строки для управления симуляторами (первая официально от Apple, вторая от Facebook, она немного удобнее в использовании)
- WebDriverAgent, также от Facebook, для того чтобы драйвить приложения вне процесса, когда приходит push-нотификация или что-то подобное
- ideviceinstaller, чтобы ставить приложение на физические девайсы и потом как-то автоматизировать на устройстве.
К моменту, когда начали писать device server, мы поисследовали. Оказывается, fbsimctl к тому моменту уже умел делать все, что умели xcrun simctl и ideviceinstaller, так что мы их просто выкинули, оставили только fbsimctl и WebDriverAgent. Это уже какое-то упрощение. Дальше мы подумали: зачем нам что-то писать, наверняка у Facebook уже все готово. И действительно, fbsimctl может работать как сервер. Можно так его запустить:
Это поднимет симулятор и запустит сервер, который будет слушать команды.
Когда вы остановите сервер, он автоматически завершит работу симулятора.
Какие команды можно отправлять? Например, с помощью curl отправить list, и он выведет полную информацию об этом устройстве:
Причем это все в JSON, то есть легко парсится из кода, и с этим легко работать. У них реализована огромная куча команд, которая позволяет делать с симулятором что угодно.
Например, approve — это дать разрешение на использование камеры, локации и нотификации. Команда open позволяет открывать deep links в приложении. Казалось бы, можно ничего не писать, а взять fbsimctl. Но оказалось, что там не хватает таких команд:
Тут легко догадаться, что без них вы не сможете запустить новый симулятор. То есть кто-то должен заранее сходить на машину и поднять симулятор. И самое важное: вы не сможете запустить тесты на симуляторе. Без этих команд сервер полностью удаленно использовать не получается, поэтому мы решили составить полный список дополнительных требований, которые нам нужны.
- Первое — это создание и загрузка симуляторов по требованию. То есть liveshot’ы могут в любой момент попросить iPhone X, а потом iPhone 5S, но при этом большинство тестов будет запускаться на iPhone 6s. Мы должны уметь по требованию создавать нужное количество симуляторов каждого типа.
- Еще мы должны как-то уметь запускать WebDriverAgent либо другие XCUI-тесты на симуляторах или физических девайсах, чтобы драйвить саму автоматизацию.
- И мы хотели полностью скрыть удовлетворение требований. Если у вас тесты хотят что-то протестировать на iOS 8 для обратной совместимости, то они не должны знать, на какую машину идти за этим устройством. Они просто запрашивают у device server iOS 8, и если такая машина есть, то он сам ее найдет, каким-то образом подготовит и вернет устройство с этой машины. Этого не было в fbsimctl.
- Наконец, это различные дополнительные действия вроде удаления cookies в тестах, что позволяет сэкономить целую минуту в каждом тесте, и другие различные хитрости, о которых мы поговорим в самом конце.
- И последний момент — это пулинг симуляторов. У нас была идея, что раз device server теперь живет отдельно от тестов, в нем можно заранее запускать все симуляторы одновременно, а когда придут тесты, симулятор уже будет готов моментально начать работать, и мы так сэкономим время. В результате мы это так и не сделали, потому что загрузка симуляторов и без того оказалось очень быстрой. И об этом тоже будет в самом конце, вот такие спойлеры.
На картинке просто пример интерфейса, мы написали какую-то обертку, клиент для работы с этим удаленным девайсом. Здесь многоточия — это различные фейсбучные методы, которые мы просто продублировали. Все остальное — собственные методы, например, быстрый сброс девайса, очистка кук и получение различной диагностики.
Вся схема выглядит примерно так: есть Test Runner, который будет запускать тесты и готовить среду; есть Device Provider, клиент к Device Server, чтобы просить у него девайсы; Remote Device — это обертка над удаленным девайсом; Device Server — сам девайс-сервер. Все позади него скрыто от тестов, там находятся какие-то бэкграунд-потоки для очистки диска и выполнения других важных действий и fbsimctl с WebDriverAgent.
Как это все работает? Из тестов или из Test Runner запрашиваем девайс с определенным capability, например iPhone 6. Запрос уходит в Device Provider, а он пересылает device server, который находит подходящую машину, запускает на ней бэкграунд-поток для подготовки симулятора и тут же возвращает в тесты некий референс, токен, обещание, что в будущем устройство будет создано и загружено. По этому токену вы можете ходить на Device Server и просить девайс. Этот токен превращаем в инстанс класса RemoteDevice и с ним можно будет работать уже в тестах.
Все это происходит почти моментально, а в фоне параллельно начинается загрузка симулятора с помощью fbsimctl. Сейчас мы, например, грузим симуляторы в headless-режиме. Если кто помнит первую картинку с железом, на ней можно было увидеть много окон симуляторов, раньше мы их грузили не в headless-моде. Они как-то грузятся, вы даже ничего не увидите. Просто ждем, пока симулятор полностью загрузится, например, в syslog появляется запись о SpringBoard и прочие эвристики для определения готовности симулятора.
Как только он загрузился, запускаем XCTest, который на самом деле поднимет WebDriverAgent, и мы начнем у него спрашивать healthCheck, потому что WebDriverAgent иногда не поднимается, особенно если система очень сильно загружена. Параллельно в это время запускается цикл, ожидающий состояние «ready» у девайса. Это на самом деле те же healthCheck. Как только девайс полностью загружен и стал готов для тестирования, вы выходите из цикла.
Теперь вы можете поставить на него приложение просто отправив запрос к fbsimctl. Тут все элементарно. Можно еще создать драйвер, запрос проксируется к WebDriverAgent, и создает сессию. После этого можно запускать тесты.
Тесты — это такая маленькая часть всей этой схемы, в них вы можете продолжать общаться с device server, чтобы совершать действия вроде удаления кук, получения видео, запуска записи и так далее. В конце надо освободить девайс (release), он завершается, у него почистятся все ресурсы, сбрасывается кэш и тому подобное. На самом деле освобождать устройство необязательно. Понятное дело, что device server сам это делает, потому что иногда тесты падают вместе с Test Runner и явным образом не освобождают девайсы. Эта схема значительно упрощена, в ней нет многих пунктов и бэкграунд-работ, которые мы выполняем, чтобы сервер мог работать целый месяц без каких-либо проблем и перезагрузок.
Результаты и следующие шаги
Самая интересная часть — это результаты. Они простые. От 30 машин перешли к 60. Это виртуальные машины, не физические машины. Самое главное, что мы сократили время с полутора часов до 30 минут. И тут возникает вопрос: если машин стало вдвое больше, то почему время уменьшилось в три раза?
На самом деле все просто. Я показывал картинку про шеринг ресурсов — это первая причина. Он дал дополнительный подъем в скорости в большинстве случаев, когда разработчики в разное время запушили задачи.
Второй момент заключается в разнесении тестов и инфраструктуры. После этого мы наконец поняли, как все работает, и смогли оптимизировать каждую из частей по отдельности и добавить еще небольшое ускорение в систему. Separation of Concerns — это очень важная идея, потому что когда все переплетено, целиком охватить систему становится невозможно.
Стало проще делать обновления. Например, когда я только пришел в компанию, мы проводили обновление на Xcode 9, который занял больше недели с тем небольшим количеством машин. Последний раз мы обновлялись на Xcode 9.2, и это прошло буквально за полтора дня, причем большая часть времени — копирование файлов. Мы не участвовали, оно само там что-то делало.
Мы значительно упростили Test Runner, в котором были rsync, ssh и прочая логика. Теперь все это выкинуто и работает где-то на *nix, в Docker-контейнерах.
Следующие шаги: подготовка device server к опенсорсу (уже после доклада он был размещён на GitHub), и подумываем об удалении ssh, потому что это требует дополнительной настройки на машинах и в большинстве случаев приводит к усложнению логики и поддержки всей системы. Но уже сейчас вы можете взять device server, разрешить на всех машинах ssh, и тесты реально пойдут на них без проблем.
Tips & tricks
Теперь самое главное — всякие хитрости и просто полезные вещи, которые мы нашли, создавая device server и эту инфраструктуру.
Первое — это самое простое. Как вы помните, у нас были MacBook Pro, все тесты запускались на лэптопах. Теперь мы их запускаем на Mac Pro.
Здесь приведены две конфигурации. Это фактически топовые версии каждого из устройств. На макбуке мы стабильно могли запускать 6 симуляторов параллельно. Если вы пытаетесь одновременно загрузить больше, симуляторы начинают проваливаться из-за того, что они сильно нагружают процессор, у них есть взаимные локи и прочее. На Mac Pro можно запустить 18 — это очень легко посчитать, потому что вместо 4 стало 12 ядер. Умножаем на три — у вас влезает примерно 18 симуляторов. На самом деле можно попробовать запустить чуть больше, но их надо как-то разнести во времени, нельзя, например, в одну минуту запускать. Хотя там будет хитрость с этими 18 симуляторами, не все так просто.
А это их цена. Я не помню, сколько это в рублях, но и так понятно, что стоят они много. Стоимость каждого симулятора у MacBook Pro обходится почти в £400, а у Mac Pro почти £330. Это уже около £70 экономии на каждом симуляторе.
Кроме того, эти макбуки приходилось ставить определенным образом, у них были зарядки на магнитах, которые приходилось приклеивать на скотч, потому что иногда они отваливались. И надо было покупать переходник, чтобы подключить Ethernet, потому что столько устройств рядом в железной коробке на Wi-Fi фактически не работают, он становится нестабилен. Переходник тоже стоит около £30, когда вы поделите на 6, то у вас получится еще по £5 на каждое устройство. Но, если вам не нужна эта супер-параллелизация, у вас всего 20 тестов и достаточно 5 симуляторов, на самом деле проще купить MacBook, потому что его можно найти в любом магазине, а Mac Pro в топовой комплектации придется заказывать и ждать. Нам они обошлись, кстати, чуть дешевле, потому что мы их брали оптом и была какая-то скидка. Еще Mac Pro можно купить с маленькой памятью, а потом апгрейдить самим, сэкономив еще больше.
Но с Mac Pro есть одна хитрость. Нам пришлось разбить их на три виртуальных машины, поставить туда ESXi. Это bare metal-виртуализация, то есть гипервизор, который ставится на голую машину, а не на хостовую систему. Он сам является хостом, поэтому мы можем запускать три виртуалки. А если вы будете ставить на macOS какую-то обычную виртуализацию, например Parallels, то вы сможете запускать только 2 виртуалки из-за лицензионных ограничений Apple. Пришлось разбивать, потому что в CoreSimulator, в основном сервисе, управляющем симуляторами, оказались внутренние локи, и одновременно больше 6 симуляторов просто не грузится, они начинают ждать чего-то в очереди, и общее время загрузки 18 симуляторов становится неприемлемым. Кстати, ESXi стоит £0, это всегда приятно, когда что-то ничего не стоит, а работает хорошо.
Почему мы не стали делать pooling? Отчасти потому, что мы ускорили сброс симулятора. Допустим, тест упал, вы хотите полностью почистить симулятор, чтобы следующий не падал из-за оставшихся непонятных файлов в файловой системе. Самое простое решение — это завершить (shutdown) симулятора, явно стереть (erase) и загрузить (boot).
Очень просто, одна строка, но занимает 18 секунд. А еще полгода или год назад занимало почти минуту. Спасибо Apple, что они оптимизировали это дело, но можно сделать хитрее. Загрузить симулятор и скопировать его рабочие директории в папочку backup. И тогда вы выключаете симулятор, удаляете рабочую директорию и копируете бэкап, запускаете симулятор.
Получается 8 секунд: загрузка ускорилась более чем в два раза. При этом ничего сложного делать не пришлось, то есть в Ruby-коде это занимает буквально две строки. На картинке я привожу пример на баше, чтобы легко было переводить в другие языки.
Следующий прием. Есть приложение Bumble, оно похоже на Badoo, но с несколько другой концепцией, гораздо более интересной. Там надо логиниться через Facebook. Во всех наших тестах, так как мы каждый раз используем нового пользователя из пула, приходилось разлогиниваться из предыдущего. Для этого мы с помощью WebDriverAgent открывали Safari, заходили на Facebook, жали Sign out. Вроде бы хорошо, но занимает почти минуту в каждом тесте. А тестов сто. Сто лишних минут.
Кроме того, Facebook любит иногда делать A/B-тесты, поэтому они могут поменять локаторы, текст на кнопках. Внезапно пачка тестов упадет, и все будут крайне недовольны. Поэтому мы через fbsimctl делаем list_apps, который находит все приложения.
Находим MobileSafari:
А там есть путь до DataContainer, а в нем есть бинарный файл с куками:
Мы просто удаляем его — это занимает 20 мс. Тесты стали проходить на 100 минут быстрее, стали стабильнее, потому что они не могут упасть из-за Facebook. Так что параллелизация иногда не нужна. Можно находить места для оптимизации, легко минус 100 минут, ничего делать не надо. В коде это две строки.
Следующее: как мы готовим хостовые машины для запуска симуляторов.
С первым примером многие, кто запускали Appium, знакомы — это отключение хардварной клавиатуры. У симулятора есть привычка при вводе текста в симуляторе подключать хардварную клавиатуру на компьютере, а виртуальную полностью скрывать. А Appium пользуется виртуальной клавиатурой для ввода текста. Соответственно, после локального дебага тестов на ввод остальные тесты могут начать проваливаться из-за отсутствия клавиатуры. Этой командой можно отключить хардварную клавиатуру, и мы делаем это перед подъемом каждой ноды для тестов.
Следующий пункт более актуален для нас, потому что приложение завязано на геолокацию. И очень часто надо запускать тесты так, чтобы изначально она была отключена. Можно в LocationMode задать 3101. Почему так? Раньше была статья в документации Apple, но потом они ее зачем-то удалили. Теперь это просто магическая константа в коде, на которую мы все молимся и надеемся, что она не сломается. Потому что как только она сломается, все пользователи окажутся в Сан-Франциско, потому что fbsimctl при загрузке ставит такую локацию. С другой стороны, мы легко об этом узнаем, потому что все будут в Сан-Франциско.
Следующее — это отключение Chrome, рамочки вокруг симулятора, которая имеет различные кнопки. При запуске автотестов она не нужна. Раньше ее отключение позволяло разместить больше симуляторов слева направо, чтобы видеть, как все идет параллельно. Теперь мы так не делаем, потому что у нас все headless. Сколько не заходи на машины, сами симуляторы видны не будут. Если же это необходимо, то можно стримить поток с нужного симулятора.
Есть еще набор разных опций, которые можно включать-выключать. Из них упомяну только SlowMotionAnimation, потому что у меня был очень интересный второй или третий день на работе. Я запустил тесты, а они все начали падать по таймаутам. Не находили элементы в инспекторе, хотя он был. Оказалось, что я в это время запустил Chrome, нажал cmd+T, чтобы открыть новую вкладку. В этот момент симулятор стал активным и перехватил команду. А для него cmd+T — это замедление всех анимаций в 10 раз для отладки анимации. Эту опцию тоже надо всегда автоматически отключать, если хотите запускать тесты на машинах, к которым имеют доступ люди, потому что они могут случайно сломать тесты замедлением анимации.
Наверное, самое интересное для меня, так как я это не так давно делал, — управление всей этой инфраструктурой. 60 виртуальных хостов (на самом деле 64 + 6 TeamCity agents) никто вручную раскатывать не хочет. Мы нашли утилиту xcversion — сейчас это часть fastlane, gem на Ruby, который можно использовать как утилиту командной строки: он частично автоматизирует установку Xcode. Дальше мы взяли Ansible, написали playbooks, чтобы раскатывать везде fbsimctl нужной версии, Xcode и деплоить конфиги для самого device server. И еще Ansible для удаления и апдейта симуляторов. Когда мы переходим на iOS 11, то оставляем iOS 10. Но когда команда тестирования говорит, что полностью отказывается от автоматического тестирования на iOS 10, мы просто проходимся Ansible и подчищаем старые симуляторы. Иначе они занимают много места на диске.
Как это работает? Если вы возьмете просто xcversion и будете вызывать его на каждой из 60 машин, это займет много времени, так как он ходит на сайт Apple и качает все образы. Чтобы обновить машины, которые есть в парке, надо выбрать одну рабочую машину, на ней запустить xcversion install нужную версию Xcode, но при этом ничего не ставить и ничего не удалять. В кэш будет скачан установочный пакет. То же самое можно сделать для любой версии симуляторов. Установочный пакет кладется в ~/Library/Caches/XcodeInstall. Дальше вы грузите все с Ceph, а если его нет, запускаете какой-нибудь web-сервер в этой директории. Я привык к Python, поэтому запускаю на машинах питоновский HTTP-сервер.
Теперь на любой другой машине разработчика или тестировщика можно сделать xcversion install и указать ссылку до поднятого сервера. Он скачает с указанной машины xip (если локальная сеть быстрая, то это произойдет фактически моментально), распакует пакет, подтвердит лицензию — в общем, сделает все за вас. Останется полностью рабочий Xcode, в котором можно будет запускать симуляторы и тесты. К сожалению, с симуляторами так удобно не сделали, поэтому приходится делать curl или wget, качать пакет с того сервера на вашу локальную машину в ту же директорию, запускать xcversion simulators --install. Мы поместили эти вызовы внутрь Ansible-скриптов и обновили 60 машин за день. Основное время заняло сетевое копирование файлов. Помимо этого, мы в этот момент переезжали, то есть часть машин отключалась. Мы перезапустили Ansible два или три раза, чтобы догнать отсутствовавшие во время переезда машины.
Небольшое подведение итогов. По первой части: мне кажется, важны приоритеты. То есть в первую очередь у вас должна быть стабильность и надежность тестов, а потом уже скорость. Если вы погонитесь только за скоростью, начнете все параллелизировать, то тесты будут работать быстро, но на них никто никогда не будет смотреть, просто все будут перезапускать, пока все внезапно не пройдет. Или вообще забивать на тесты и пушить в мастер.
Следующий момент: автоматизация — это та же самая разработка, поэтому можно просто взять паттерны, которые уже придумали за нас, и использовать их. Если сейчас ваша инфраструктура тесно связана с тестами и планируется масштабирование, то это хороший момент, чтобы сперва разделить, а потом уже масштабировать.
И последний пункт: если стоит задача ускорить тесты, то первым в голову приходит добавить еще симуляторов, чтобы на какой-то фактор стало быстрее. На самом деле очень часто надо не добавлять, а внимательно проанализировать код и парой строк все оптимизировать, как в примере с cookies. Это лучше параллелизации, потому что двумя строками кода было сэкономлено 100 минут, а для параллелизации придется написать много кода и после поддерживать железную часть инфраструктуры. По деньгам и ресурсам это обойдется значительно дороже.
Тех, кого заинтересовал этот доклад с конференции Heisenbug, может также заинтересовать следующий Heisenbug: он пройдёт в Москве 6-7 декабря, и на сайте конференции уже есть описания ряда докладов (и, кстати, приём заявок на доклады ещё открыт).