ИНЖЕНЕРНАЯ ЗАМЕТКА

Как разделить тесты Xcode на облачном Mac и переиспользовать сборку

Как разделить тесты Xcode на облачном Mac и переиспользовать сборку

Когда проект iOS содержит тысячи модульных и UI-тестов, прямой запуск xcodebuild test обычно объединяет в одной задаче компиляцию, запуск симулятора, установку приложения и выполнение всех тестов. Сбой может произойти в последние минуты, но при повторном запуске компиляция начнётся заново. На облачном Mac такую цепочку эффективнее разделить на два этапа: один раз подготовить сборку для тестирования, а затем переиспользовать её в нескольких группах тестов с чётко заданными границами.

Разделите сборку и выполнение тестов

Команда Xcode build-for-testing собирает приложение, хост тестов и тестовые пакеты, а затем создаёт файл .xctestrun с описанием среды выполнения. Последующие запуски можно выполнять через test-without-building, чтобы для каждой группы не разрешать зависимости и не компилировать исходный код заново.

Сначала зафиксируйте рабочее пространство, Scheme, целевое устройство и путь к DerivedData:

set -euo pipefail

DERIVED_DATA="$PWD/.ci/DerivedData"
RESULTS="$PWD/.ci/Results"
DESTINATION="platform=iOS Simulator,name=iPhone 16,OS=latest"

rm -rf "$DERIVED_DATA" "$RESULTS"
mkdir -p "$RESULTS"

xcodebuild build-for-testing \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination "$DESTINATION" \
  -derivedDataPath "$DERIVED_DATA" \
  CODE_SIGNING_ALLOWED=NO

find "$DERIVED_DATA/Build/Products" -name "*.xctestrun" -print

Параметр CODE_SIGNING_ALLOWED=NO подходит только для тестов в симуляторе, которым не требуется подпись для физического устройства. Если тестовая цель содержит компоненты, которые необходимо подписывать, удалите этот параметр и используйте штатную конфигурацию подписи проекта. После успешной сборки не перемещайте DerivedData: файл .xctestrun может содержать абсолютные пути к расположенным там файлам.

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

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

Не стоит вручную делить тесты на группы «примерно по сто методов». Имена методов часто меняются, поэтому поддерживать такое разбиение сложно. Надёжнее группировать тесты по тестовой Target, тестовому классу или бизнес-домену и хранить списки в репозитории.

Группа Рекомендуемая область Подходящие тесты
unit-core Уровень чистой логики Преобразование данных, валидация, конечные автоматы
unit-storage Уровень хранения данных Базы данных, кеши, миграции
ui-account Сценарии учётной записи Вход, настройки, экраны разрешений
ui-checkout Сценарии транзакций Выбор товара, подтверждение и обработка ошибок

Список можно хранить в обычном текстовом файле:

AppTests/ParserTests
AppTests/SessionReducerTests
AppTests/ValidationTests

При запуске преобразуйте каждую строку в отдельный параметр -only-testing:. Идентификатор теста обычно имеет формат Target/Class или Target/Class/testMethod. Сначала выполните xcodebuild -list, чтобы проверить Scheme, а затем небольшой командой проверьте идентификаторы. Это позволит избежать ситуации, когда из-за опечатки группа фактически не запускает ни одного теста.

Запустите одну группу отдельно

XCTESTRUN="$(find "$DERIVED_DATA/Build/Products" -name '*.xctestrun' -print -quit)"

xcodebuild test-without-building \
  -xctestrun "$XCTESTRUN" \
  -destination "$DESTINATION" \
  -parallel-testing-enabled NO \
  -only-testing:AppTests/ParserTests \
  -only-testing:AppTests/SessionReducerTests \
  -resultBundlePath "$RESULTS/unit-core.xcresult"

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

Выделите каждому параллельному заданию отдельный симулятор

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

Сначала просмотрите доступные среды выполнения:

xcrun simctl list runtimes
xcrun simctl list devicetypes

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

xcrun simctl create \
  ci-shard-1 \
  com.apple.CoreSimulator.SimDeviceType.iPhone-16 \
  "$RUNTIME_ID"

Сохраните полученный UDID в контролируемой конфигурации запуска. Перед каждым запуском выключайте устройство и очищайте его состояние:

xcrun simctl shutdown "$SIMULATOR_UDID" 2>/dev/null || true
xcrun simctl erase "$SIMULATOR_UDID"
xcrun simctl boot "$SIMULATOR_UDID"
xcrun simctl bootstatus "$SIMULATOR_UDID" -b

Затем измените destination на platform=iOS Simulator,id=$SIMULATOR_UDID. Если заданию требуются заранее настроенные язык, разрешения или тестовые данные, добавляйте их единообразно после erase, а не полагайтесь на состояние, оставшееся от предыдущего запуска.

Определяйте уровень параллелизма по нагрузке на ресурсы

Если явно запустить две группы, а затем включить встроенное параллельное тестирование Xcode внутри каждой из них, возникнет вложенный параллелизм. Количество симуляторов, тестовых процессов и вспомогательных процессов компиляции будет расти одновременно, и в итоге конвейер может оказаться медленнее последовательного запуска. В первой версии следует установить -parallel-testing-enabled NO и добавлять только по одному слоту за раз.

Отслеживайте как минимум три категории показателей:

Не следует стремиться только к одинаковому количеству тестов в группах. Класс UI-тестов с большим числом перезапусков приложения может выполняться дольше, чем сотни тестов чистых функций. Списки можно корректировать по длительности нескольких последних запусков конвейера, однако не распределяйте тесты случайным образом во время выполнения: это затруднит воспроизведение сбоев и сравнение тенденций.

Обеспечьте независимый перезапуск и сбор данных о сбоях

Для каждой группы необходимо сохранять код завершения, консольный журнал и отдельный файл .xcresult. Итоговый этап конвейера может завершиться с ошибкой, но сбой первой группы не должен приводить к удалению данных остальных групп. При повторном запуске группы со сбоем продолжайте использовать исходный .xctestrun, но укажите новый путь для результатов:

xcodebuild test-without-building \
  -xctestrun "$XCTESTRUN" \
  -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
  -parallel-testing-enabled NO \
  -only-testing:AppUITests/CheckoutTests \
  -resultBundlePath "$RESULTS/ui-checkout-retry-1.xcresult"

Если изменились исходный код, параметры компиляции, версия Xcode или среда выполнения симулятора, необходимо снова выполнить build-for-testing. Старые продукты сборки нельзя использовать как подтверждение результатов для нового коммита. Сохраняйте списки тестов, журналы сборки, результаты групп и версии окружения, чтобы определить источник сбоя: код, состояние тестов или среду выполнения.

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

Часто задаваемые вопросы

Чем явное разделение отличается от параллельного тестирования Xcode?

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

Нужно ли пересобирать проект после сбоя одной группы?

Нет, если тестовые продукты, файл xctestrun и версия среды не изменились. Тогда достаточно повторить только неудачную группу через test-without-building.

VMDebug облачные Mac

Нужен физический Mac mini, выделенный под один заказ?

Сравните конфигурации M4 и M4 Pro, четыре периода аренды и 5 доступных узлов, а затем выберите устройство под свой рабочий процесс.

Выбрать конфигурацию и заказать