
Полнота покрытия проекта тестами во многом предопределяет его качество, надёжность и безопасность, поэтому команды предсказуемо стремятся писать тесты на все методы и компоненты. Это имеет и обратную сторону: увеличение покрытия удлиняет прогоны, что, в свою очередь, сказывается на продуктовых метриках. В результате при разработке возникает необходимость искать баланс между покрытием и способами ускорения прогонов. Здесь у каждой компании свой подход.
Меня зовут Мария Рогова. Я iOS-разработчик в ОК. В этой статье я расскажу, с чего мы начинали, почему требовалась оптимизация и что мы предпринимали для ускорения прогона UI-тестов в iOS.
Отправная точка: январь 2024 года
iOS-приложение ОК — это комплексный продукт, объединяющий множество востребованных функций: от ленты новостей и мессенджера до видеостриминга и игр. Чтобы обеспечить стабильную работу всей этой функциональности, мы уделяем особое внимание тестированию, стремясь к максимальному покрытию кода.
Так, в январе 2024 года у нас на iOS было 700 UI KIF-тестов. При этом:
- на сборку приложения и тестов требовалось 10 минут; 
- тесты гоняли пять Mac Mini; 
- один Mac Mini «контролировал» запуск тестов; 
- прогон тестов занимал 70 минут; 
- на весь процесс от запуска сборки до получения результатов тестов уходило 85 минут. 
При этом мы понимали, что общее время прогона тестов можно и важно оптимизировать, в том числе, чтобы:
- уменьшить time-to-market; 
- раньше обнаруживать ошибки и, соответственно, дешевле и быстрее их исправлять; 
- выстраивать Quality Gates. 
Поэтому мы начали изучать, что именно можно улучшить.
Анализ схемы запуска
Прежде всего, чтобы определить точки роста в наших процессах, мы с командой начали изучать актуальную схему запуска. Весь процесс у нас состоит из нескольких этапов:
- запуск через TeamCity; 
- сборка приложения с тестами; 
- загрузка сборки в хранилище; 
- прогон тестов; 
- получение отчёта. 
При этом мы хотели:
- писать новые тесты на XCUI, что увеличивало время прогона на 10 минут (до 95); 

- увеличить количество тестов; 
- запускать тесты на каждый pull request; 
- кастомизировать запуски; 
- применять тесты только к изменениям. 
Исходя из этого, мы начали оптимизировать прогоны.
Приступаем к оптимизации
Самый простой и очевидный способ ускорения прогонов — закупка и подключение дополнительных машин. Но закупка оборудования требует времени, а его использование — предварительной настройки. Поэтому одновременно с закупкой мы начали оптимизировать процессы.
Работа с артефактами
Начали с работы с артефактами. Так, с добавлением этапа сборки XCUI-тестов и приложения мы получили схему запуска следующего вида:

При этом в такой реализации на каждый коммит в любой из веток мы собираем приложение с KIF-тестами и собираем XCUI-тесты с основным приложением, независимо от того, был уже прогон или нет. То есть в любом случае тратим на эти этапы 20 минут.
Чтобы уйти от подобного дублирования, мы решили кешировать собранные приложения. В таком случае схема запуска преобразовывается:

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

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

Благодаря этому мы сэкономили 17 минут.
Но и этого нам показалось недостаточно. В итоге мы решили оптимизировать и этап передачи артефактов из одной машины в другую, для чего изменили способ их загрузки и скачивания. Так мы ускорили прогон ещё на 2,5 минуты.

Ускорение прогона тестов
Самым ресурсоёмким этапом в нашей схеме является сам прогон тестов — изначально на него тратили 70 минут. Но дальше — больше. Так, за время оптимизации количество iOS-тестов у нас увеличилось, поэтому и длительность прогона выросла до 80 минут.
Как я уже упоминала ранее, вначале мы заказали дополнительные машины. Таким образом, к уже используемым шести Mac Mini мы добавили ещё шесть. Мы ожидали, что скорость прогона вырастет в два раза, но этого не произошло: длительность прогона снизилась до 50 минут вместо ожидаемых 40.
Тогда мы начали анализировать, от чего зависит длительность прогона тестов. Основных факторов несколько:
- количество тестов; 
- их длительность; 
- раннер и его инфраструктура; 
- Apple. 
Каждый из них мы начали отрабатывать отдельно.
Количество тестов
Наш продукт развивается, поэтому очевидно, что количество тестов будет постепенно увеличиваться, как и длительность их прогона. Поэтому мы решили не запускать все тесты. Вместо этого мы можем делать кастомизированные запуски. Например, по необходимости запускать:
- все тесты; 
- выборочные тесты; 
- тесты под конкретный фреймворк (KIF или XCUI); 
- отдельно smoke-тесты; 
- требуемые тестовые наборы. 
Помимо этого, в перспективе мы хотим внедрить impact-анализ, чтобы запускать тесты только применительно к определённым изменениям в коде.
Длительность тестов
К уменьшению длительности тестов мы также подошли комплексно. В первую очередь начали работу с фреймворком автотестов: перешли от фиксированных ожиданий wait(5) к использованию XCTWaiter и XCTestExpectation, то есть ждём не строго пять секунд, а пять и менее.
Далее мы поменяли подход к поиску элементов. Изначально для поиска каждого элемента в XCUI использовали полный путь от XCUIApplication. Мы изменили подход: теперь сначала находим контейнер (например, ячейку), а уже внутри него ищем нужные нам компоненты. Это ускоряет поиск и делает тесты стабильнее.  
Затем мы изменили логику работы с приложением: перешли на deep link’и, а для подготовки данных начали использовать API-методы.
При этом важно, что QA-инженеры сопровождали любые изменения, что позволило реализовать их без снижения стабильности тестов, которая у нас сейчас на уровне 97 %.
Раннер и его инфраструктура
В рамках ускорения работы с раннером и инфраструктурой мы сначала добавили сортировку тестов по времени. Теперь самые продолжительные тесты запускаются в первую очередь, а короткие — в конце. Такая приоритизация позволила нам ускорить прогон на четыре минуты.
Далее мы приступили к изменению работы с симуляторами.
Поскольку каждый запуск симулятора занимает около 1,5 минуты, мы их не закрываем после прогона, а переиспользуем.
Одновременно с этим мы стали чётко регламентировать оптимальное количество симуляторов, поскольку столкнулись с тем, что при запуске, например, на пяти симуляторах стабильность тестов снижается, а скорость прогона падает.
При этом мы начали запускать тесты, только если симуляторы уже открыты. Для этого мы ввели обязательную задержку перед началом теста продолжительностью три минуты, рассчитывая, что за это время все симуляторы успешно запустятся. Несмотря на дополнительное ожидание, такая мера позволила сэкономить около 4–5 минут благодаря стабилизации процесса. Ведь ранее, если запуск происходил раньше завершения загрузки симуляторов, тесты часто завершались ошибками.
Также важно, что симуляторы запускаются в Headless-режиме, который на 25 % менее ресурсоёмкий, что тоже ускоряет прогон тестов.
При этом мы не удаляем приложение после тестов, а просто очищаем его после каждого прогона от UserDefaults, Documents и Caches. 
UserDefaults:
xcrun simctl spawn booted defaults delete <bundle id>Documents и Caches:
xcrun simctl get_app_container booted <bundle id> data
rm -rf "$(xcrun simctl get_app_container booted <bundle id> data)/Documents»
rm -rf "$(xcrun simctl get_app_container booted <bundle id> data)/Library/Caches"Помимо этого мы проставляем разрешения для тестов. Поэтому в каждом тесте нет оповещений, и это тоже экономит нам время.
Суммарно оптимизация работы с симуляторами позволила нам сократить длительность прогона ещё на 10 минут.
Apple
На решения Apple мы, очевидно, повлиять не можем. При этом вполне прогнозируемо, что каждый следующий симулятор будет сложнее предыдущего, будет требовать больше вычислительных ресурсов, и времени на прогон тестов на имеющихся у нас машинах будет уходить больше.
Вместе с тем мы понимаем эту тенденцию, поэтому стараемся комплексно и заблаговременно адаптироваться к потенциальным вызовам.
Итоги и планы на будущее
За полтора года мы проделали большую работу, в ходе которой значительно оптимизировали схему запуска и вдвое увеличили количество машин (с шести до двенадцати Mac Mini).

Проделанная работа позволила нам получить ощутимый прирост скорости прогона iOS-тестов. Так, если в 2024 году у нас было всего 700 KIF-тестов и лучшая длительность их прогона составляла 83 минуты, то уже сейчас мы гоняем 700 KIF и 600 XCUI-тестов всего за 33 минуты, что практически в 2,5 раза быстрее, причём без потери точности и стабильности тестов.
Мы не останавливаемся на достигнутом. В наших планах реализация:
- запуска тестов на каждый pull request; 
- impact-анализа; 
- предварительного открытия симуляторов; 
- ускорения опроса элементов. 
О том, каких результатов это позволит нам достичь, обязательно расскажем в одной из следующих статей.
 
          