Тестовый набор в GitHub Actions растёт, а Pull Request всё дольше ждёт результата из-за очереди, общего Simulator или одного перегруженного Runner.

Самое быстрое решение — сначала разделить XCTest по стабильным границам через Xcode Test Plans или only-testing, затем запустить шарды через matrix на нескольких независимых удалённых Mac; один Runner не создаёт межузловой параллелизм.

Эта статья предназначена для трёх групп:

  • iOS-инженеров и тестировщиков, которым нужно разделить UI-набор на независимые и воспроизводимые части;
  • DevOps-инженеров, настраивающих GitHub Actions matrix, метки Runner и сбор тестовых артефактов;
  • руководителей разработки, которым нужно понять, увеличивать ли число удалённых Mac или сначала исправлять тесты и очередь.
01

Базовая линия перед разбиением

Как GitHub Actions запускает несколько наборов iOS UI-тестов параллельно?
Каждый шард должен стать отдельным Job матрицы, а каждый Job — попасть на свободный Runner с подходящими Xcode, Simulator и правами проекта. Количество элементов в matrix показывает число созданных заданий, но не число реально выполняющихся тестов. Фактический параллелизм ограничивают свободные Runner, правила маршрутизации и ресурсы самих Mac.

Сначала выполните полный запуск без изменений. Зафиксируйте:

  • commit или точную ревизию проекта;
  • Scheme и Test Plan;
  • порядок запуска тестов;
  • длительность подготовки сборки отдельно от времени UI-тестов;
  • длительность отдельных групп;
  • очередь перед получением Runner;
  • имя Simulator и его состояние;
  • путь к .xcresult;
  • количество падений, повторных запусков и инфраструктурных ошибок.

Это не формальность. Если ожидание Runner занимает большую часть цикла, разбиение тестов не устранит задержку. Если тесты быстро стартуют, но долго работают на одном Simulator, сначала нужно исследовать ресурсы и состояние тестового кода.

GitHub описывает Job как отдельную единицу работы, которую workflow передаёт доступному Runner. Поэтому официальное описание устройства GitHub Actions полезно использовать для разделения трёх показателей:

  • параллельность Job в GitHub Actions;
  • одновременные процессы или Simulator внутри одного Mac;
  • внутреннее выполнение тестов средствами Xcode и Swift Testing.

Это разные уровни. Увеличение matrix не добавляет физические машины. Запуск нескольких Simulator на одном Mac не равен запуску на нескольких узлах. Параллельность внутри тестового процесса также не доказывает, что UI-тесты безопасно делить.

Перед дальнейшими шагами установите контрольную точку:

  1. полный набор запускается на исходной конфигурации;
  2. все тестовые результаты сохраняются;
  3. известны самые долгие группы;
  4. отделены ошибки теста от ошибки Runner;
  5. зафиксированы входные данные и аккаунты.

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

02

Границы XCTest и Xcode Test Plans

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

Используйте один из трёх вариантов:

  • отдельные Test Plan с самостоятельными наборами тестов;
  • Test Target или Suite, если границы уже отражают архитектуру проекта;
  • only-testing, когда нужно временно выбрать конкретный класс или метод без создания новой долгосрочной структуры.

Apple рекомендует организовывать тесты так, чтобы обратная связь приходила быстрее и причины отказа было проще определить. В документации Apple по организации тестов описаны Test Plans и способы управлять составом тестов. Для CI это означает: каждый шард должен иметь понятный владелец, фиксированный вход и отдельную команду воспроизведения.

Сохраните в каждом шарде:

  • идентификатор, например ui-auth или ui-checkout;
  • список тестов;
  • Test Plan либо набор only-testing;
  • требуемый тип устройства;
  • тестовый аккаунт;
  • внешние зависимости;
  • ожидаемый профиль нагрузки;
  • отдельный путь для результата.

Не объединяйте в разные шарды тесты, которые используют одну изменяемую учётную запись, общий push-контур или последовательный бизнес-сценарий. Например, тест создания заказа и тест проверки его последующего статуса могут требовать общего состояния. Механическое распределение по файлам в таком случае создаст гонки, а не ускорение.

Как группировать UI-тесты по Test Plan?
Сначала сделайте группы по функциональной зависимости и окружению, затем проверьте каждую группу отдельно. После этого выполните полный набор и сравните список тестов. В нём не должно быть пропусков или повторов. Для временного эксперимента можно использовать -only-testing:<Target>/<TestClass> или -only-testing:<Target>/<TestClass>/<testMethod>, но точный синтаксис перед запуском проверьте через xcodebuild -help на целевом узле.

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

Условия остановки первого этапа

Остановитесь и вернитесь к структуре тестов, если:

  • один шард содержит скрытую зависимость от другого;
  • полный запуск показывает пропущенные тесты;
  • один и тот же тест появляется в нескольких группах без намеренной причины;
  • результат зависит от порядка Job;
  • отдельный шард невозможно повторить одной командой.

Пока эти условия не устранены, добавлять Runner рано.

03

Матрица GitHub Actions и маршрутизация Runner

После проверки границ превратите каждый шард в элемент matrix. В рабочем процессе это обычно означает, что Job получает параметры shard_id, test_plan, destination и путь к результатам. Названия ниже условные:

strategy:
  matrix:
    shard: [ui-auth, ui-catalog, ui-checkout]
  max-parallel: 2

Значения должны соответствовать вашей структуре, а не копироваться буквально. Полный синтаксис стратегии, matrix и ограничений параллельности проверяйте в официальной документации GitHub Actions Workflow syntax.

Для self-hosted Runner используйте метки. Например, Job может требовать метки операционной системы, архитектуры и подготовленной среды. GitHub выбирает Runner, который соответствует всем указанным требованиям. Документация GitHub по меткам self-hosted Runner описывает этот механизм маршрутизации.

Разделяйте следующие ограничения:

  • max-parallel ограничивает одновременно запущенные элементы матрицы;
  • доступное число подходящих Runner определяет реальную ёмкость;
  • concurrency управляет конкурирующими запусками workflow или группами;
  • очередь возникает, если Job уже создан, но подходящего Runner нет;
  • права проекта, сертификаты и Simulator могут сделать формально свободный узел непригодным.

Когда один Mac Runner способен выполнять несколько Simulator-задач?
Точного универсального числа нет. Оно зависит от проекта, выбранных устройств, версии Xcode, памяти, диска, сетевого поведения и степени изоляции. Один Mac может принять несколько Job только при наличии нескольких независимых сред и достаточных ресурсов, но сам факт запуска нескольких процессов не означает стабильного параллелизма.

Сначала ограничьте max-parallel консервативным значением и выполните короткий пробный запуск. Затем изменяйте только один уровень: либо число Job на узел, либо число узлов, либо внутреннюю параллельность Xcode. Иначе вы не узнаете, какая перемена вызвала нестабильность.

Повторная сборка или общий build-for-testing

У вас есть два основных пути.

Отдельная сборка в каждом шарде

Плюсы:

  • проще отлаживать;
  • меньше зависимости от передачи артефактов;
  • каждый Job самодостаточен;
  • ошибка сборки сразу привязана к конкретному Runner.

Минусы:

  • увеличивается повторная работа;
  • расходуется больше времени и диска;
  • различия среды могут проявиться в разных Job.

Одна сборка и последующее тестирование

Плюсы:

  • тестовые Job используют один набор собранных продуктов;
  • уменьшается повторная компиляция;
  • легче сравнивать результаты шарды.

Минусы:

  • нужно надёжно передать и распаковать продукты;
  • появляется дополнительный этап хранения;
  • несовпадение Xcode, SDK или архитектуры может сделать артефакт непригодным;
  • ошибка упаковки блокирует несколько тестовых Job.

Используйте build-for-testing и test-without-building только после проверки, что Runner имеют совместимую среду. Не называйте путь «быстрее» без сравнения полного цикла: сборка, очередь, передача, тестирование, загрузка результатов.

04

Изоляция Simulator и рабочей директории

Параллельные Job чаще ломаются не в matrix, а в общей среде. Для каждого шарда задайте отдельные:

  • имя или destination Simulator;
  • DerivedData;
  • путь .xcresult;
  • временную директорию;
  • порты локальных сервисов;
  • тестовую учётную запись;
  • каталог скриншотов и логов;
  • ключи и переменные окружения, где это допустимо.

Нельзя одновременно писать несколько результатов в один путь. Нельзя полагаться на «текущий выбранный Simulator» на машине. Destination должен быть задан явно, а состояние устройства перед тестом — проверено.

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

  • загрузку CPU и памяти;
  • свободное дисковое пространство;
  • время boot и shutdown Simulator;
  • длительность тестовых шагов;
  • число зависших процессов;
  • ошибки доступа к портам;
  • повторы и скриншоты падений.

После этого перенесите те же Job на разные удалённые Mac. Такой порядок помогает отличить ограничение одного узла от ошибки теста.

Важно: один удачный параллельный запуск не подтверждает готовность CI. Для допуска нужен повторяемый результат на разных Pull Request и с теми же входными данными; успешный retry не должен стирать первую ошибку.

На одном Mac внутреннее выполнение Xcode может конкурировать за CPU, память, диск и Simulator service. На нескольких Mac конкуренция перемещается в сеть, очередь Runner, хранилище артефактов и общие тестовые системы. Поэтому «больше Simulator» и «больше Mac» — разные решения.

05

Сборка xcresult и классификация отказов

Каждый Job должен загружать собственный комплект:

  • .xcresult;
  • консольный лог xcodebuild;
  • скриншоты и видео, если они настроены;
  • идентификатор шарда;
  • список выбранных тестов;
  • destination;
  • commit и сведения о среде;
  • признак первой попытки или retry.

Результат нельзя заменять только зелёным статусом Job. В финальном этапе проверьте четыре условия:

  1. каждый ожидаемый шард завершил работу;
  2. ни один результат не потерян;
  3. инфраструктурные ошибки отделены от assertion failure;
  4. список тестов совпадает с планом.

Как объединять несколько xcresult и находить причину падения?
Не складывайте каталоги вслепую и не объединяйте проценты покрытия арифметически. Сначала сохраните каждый пакет отдельно, затем используйте поддерживаемый Apple инструмент или команду, подтверждённую локальным xcrun help и документацией вашей версии Xcode. Для анализа сопоставляйте имя теста, шард, commit, устройство и первую ошибку.

В материалах Apple о результатах тестирования описаны данные, которые Xcode показывает при интерпретации результатов. Это важно для разделения:

  • падения assertion;
  • тайм-аута;
  • сбоя запуска приложения;
  • недоступности Simulator;
  • ошибки подписи или разрешений;
  • нехватки диска;
  • сетевой ошибки тестового сервиса.

Retry нужен для диагностики нестабильности, но не для сокрытия отказа. Если первая попытка красная, а повторная зелёная, сохраняйте обе. Такой тест отправляйте в отдельный список flaky и назначайте владельца. Иначе матрица даст красивый итоговый статус, но не покажет реальное качество набора.

Покрытие также требует осторожности. Если разные шарды запускают тесты на одном и том же бинарном коде, данные покрытия могут пересекаться. Проценты нельзя просто суммировать. Выберите официальный способ объединения, проверьте его на контрольном запуске и зафиксируйте правило в CI.

06

Решающий список для выбора схемы

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

  • [ ] Полный серийный запуск сохранён вместе с .xcresult, логами и перечнем тестов.
  • [ ] Каждый шард запускается отдельной командой и не зависит от порядка других Job.
  • [ ] Для каждого шарда определены собственные Simulator, DerivedData, временный каталог и путь результата.
  • [ ] Два шарда уже прошли пробный запуск без пропусков и взаимного перезаписывания данных.
  • [ ] На всех подходящих Runner совпадают Xcode, SDK, архитектура и права проекта.
  • [ ] Первая ошибка сохраняется даже после успешного повторного запуска.
  • [ ] Очередь Runner измеряется отдельно от времени сборки и выполнения UI-тестов.

Выбирайте вариант по результату проверки:

  • Если отмечены все пункты и очередь Runner регулярно определяет длительность workflow, выбирайте несколько независимых удалённых Mac и запускайте шарды через matrix.
  • Если пункты про изоляцию или повторяемость не отмечены, оставьте один Runner и исправьте Test Plans, общее состояние и пути артефактов.
  • Если шарды запускаются, но один из них значительно дольше остальных, переразделите набор по зависимости и фактическому времени, а не добавляйте новые узлы.
  • Если очередь мала, но Simulator нестабилен, уменьшите параллелизм на одном Mac и исследуйте CPU, память, диск и состояние устройства.
  • Если результаты серийного и параллельного запуска ещё не сопоставлены, сохраните серийный или двойной контур для релизных проверок.
  • Если причиной задержки является только временный всплеск Pull Request, арендуйте дополнительный удалённый Mac на период проверки; для постоянной предсказуемой нагрузки отдельно сравните аренду с покупкой собственного оборудования.

Этот список даёт обратный путь: при первом нестабильном запуске отключите матрицу, вернитесь к серийному базовому Job и сравните первую ошибку с сохранёнными результатами. Без такой точки отката масштабирование превращается в угадывание.

07

Решение о переходе к нескольким Mac

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

  • Если ожидание подходящего Runner остаётся основной частью цикла, то проверьте доступное число независимых удалённых Mac и только затем увеличивайте ёмкость.
  • Если очередь мала, но один шард намного длиннее остальных, то переразделите тесты по времени и зависимостям.
  • Если ошибки появляются только при совместной работе на одном узле, то уменьшите внутренний параллелизм и усилите изоляцию.
  • Если падения повторяются на одном тесте и одном входе, то исправляйте XCTest, а не добавляйте Runner.
  • Если инфраструктурные ошибки исчезают на отдельном узле, то сравните Simulator, DerivedData, диск и локальные сервисы.
  • Если критические тесты ещё не доказали эквивалентность результатов, то оставьте серийный контрольный запуск.
  • Если полный набор нужен перед релизом, то сохраните двойной контур: быстрые шарды для Pull Request и периодический полный запуск.

Практический сценарий выглядит так:

  1. Выполните полный серийный запуск и сохраните артефакты.
  2. Создайте два шарда по границам Test Plan или only-testing.
  3. Запустите их на одном Runner с изолированными путями.
  4. Сравните результаты с серийным запуском.
  5. Перенесите те же Job на независимые удалённые Mac.
  6. Добавьте третий шард только после проверки первых двух.
  7. Наблюдайте очередь, дисбаланс, повторяемость и пропуски.
  8. Оставьте способ быстро отключить matrix и вернуться к серийному режиму.

Для временной или проектной CI-нагрузки вы можете рассмотреть удалённый Mac с KVMNODE, но сначала проверьте доступность нужной версии Xcode, права, сетевой маршрут, чистоту Simulator и правила хранения секретов. Если вам нужен именно Mac mini как постоянный узел, сравните требования проекта с вариантами Mac mini для разработки, а не только с числом параллельных Job.

Сигналы для пересмотра схемы

Перестройте шарды, если самый долгий Job постоянно определяет время всего workflow. Сравнивайте не только среднее время. Нужны также:

  • время до назначения Runner;
  • длительность каждого шарда;
  • разница между самым быстрым и самым медленным;
  • доля инфраструктурных отказов;
  • количество повторных запусков;
  • состояние диска и памяти;
  • время загрузки артефактов;
  • результат серийной контрольной проверки.

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

08

Выбор между текущей схемой и удалённым Mac

Если вы сейчас запускаете UI-тесты на одном локальном Mac, основные ограничения обычно связаны с занятостью рабочего компьютера, невозможностью держать Runner постоянно доступным и конфликтом между разработкой и CI. Если используется обычный Linux-сервер, к этим проблемам добавляется отсутствие нативного macOS-инструментария, Simulator и Xcode. Если вы пытаетесь заменить среду виртуальной машиной или нестабильной неофициальной конфигурацией, усложняются обновления, подпись, диагностика и воспроизводимость.

Удалённый Mac не отменяет работу с Test Plans и изоляцией. Он даёт отдельную macOS-среду, которую можно подготовить под Runner, оставить доступной для CI и подключать на срок эксперимента или проекта. Для сценария с растущим UI-набором это часто практичнее, чем покупать отдельный компьютер до того, как вы измерили реальную потребность в параллельных узлах.

Начните с одного серийного базового запуска и двух изолированных шардов. Если измерения покажут, что время теряется именно в очереди Runner, а не в общих данных или сбоях Simulator, переходите к проверке удалённого Mac для Xcode и Simulator и выбирайте независимые узлы под короткий тестовый период. Для долгой стабильной нагрузки с постоянными требованиями к железу покупка собственного Mac может быть рациональнее; для проверки архитектуры, временного релиза или всплеска Pull Request аренда через KVMNODE позволяет сначала подтвердить модель измерениями, а не предположениями.