Руководство

Автоматизируем UI-тесты iOS через XCUITest на выделенном Mac

Чтобы автоматизировать XCUITest, запускайте xcodebuild test с симулятором в destination и параметром -resultBundlePath на Mac, где выполнен вход пользователя. Создайте отдельный симулятор через xcrun simctl. Добавьте -parallel-testing-enabled YES, чтобы распределить тесты по клонам симулятора, и -retry-tests-on-failure, чтобы перезапускать нестабильные тесты. Каждую команду здесь мы запускали на Mac с Xcode 26.6 и симулятором iOS 26.5.

Что нужно заранее

  • Mac с Xcode и хотя бы одной средой выполнения симулятора iOS.
  • Таргет UI-тестов в проекте и общая (shared) схема, которая его тестирует.
  • Сессия рабочего стола с выполненным входом. UI-тесты управляют приложением Simulator, поэтому всё, что их запускает, должно жить в этой сессии. Значит, агент CI запускается как LaunchAgent, а не как LaunchDaemon. Шаблон plist от Buildkite говорит то же самое: графический режим позволяет UI-тесты Xcode, но требует входа.

1. Проверьте Xcode и среды выполнения

xcodebuild -version
sudo xcodebuild -runFirstLaunch
xcrun simctl list runtimes
xcrun simctl list devicetypes | grep iPhone

-runFirstLaunch устанавливает пакеты и принимает лицензию. Запускайте его после каждой установки или обновления Xcode.

2. Создайте симулятор только для CI

Отдельное устройство отделяет CI от всего, что человек открывал вручную. bootstatus -b загружает его и ждёт готовности.

UDID=$(xcrun simctl create "CI iPhone 17" "iPhone 17" com.apple.CoreSimulator.SimRuntime.iOS-26-5)
xcrun simctl bootstatus "$UDID" -b
echo "$UDID"

Используйте UDID в destination: -destination "platform=iOS Simulator,id=$UDID". Имя тоже подойдёт, например name=iPhone 17,OS=26.5, но у двух устройств может быть одно имя.

3. Запустите UI-тесты с result bundle

rm -rf build/TestResults.xcresult
xcodebuild test \
  -project MyApp.xcodeproj \
  -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" \
  -derivedDataPath build/DerivedData \
  -resultBundlePath build/TestResults.xcresult

В result bundle хранятся все результаты тестов, логи, скриншоты и сбои. Сводку можно прочитать в командной строке:

xcrun xcresulttool get test-results summary --path build/TestResults.xcresult

Она выводит JSON с числом пройденных, упавших и пропущенных тестов для каждого устройства. Прикрепите bundle к запуску CI как артефакт, сжав его через ditto -c -k --keepParent.

4. Соберите один раз, тестируйте много раз

Отделите сборку от запуска тестов. Тогда часть тестов можно перезапустить без повторной компиляции.

xcodebuild build-for-testing -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData

xcodebuild test-without-building -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData \
  -only-testing:MyAppUITests/LoginTests/testSignIn

-only-testing принимает Target, Target/Class или Target/Class/method. -skip-testing работает так же, но наоборот.

5. Запускайте тесты параллельно

xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" \
  -parallel-testing-enabled YES \
  -parallel-testing-worker-count 2 \
  -resultBundlePath build/TestResults.xcresult

Xcode клонирует симулятор и распределяет классы тестов по клонам. В нашем логе тесты шли на Clone 1 of iPhone 17. -parallel-testing-enabled переопределяет настройку в схеме. На Mac с 16 GB начните с 2 воркеров и замерьте, прежде чем увеличивать. Каждый клон это полноценный симулятор в памяти. Чтобы тестировать на нескольких типах устройств сразу, перечислите несколько destination и задайте -maximum-concurrent-test-simulator-destinations.

6. Повторяйте нестабильные тесты

xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" \
  -retry-tests-on-failure \
  -test-iterations 3 \
  -resultBundlePath build/TestResults.xcresult

xcodebuild -help пишет, что упавший тест перезапускается до заданного числа итераций. Без -test-iterations максимум равен 3. Мы проверили это на тесте, который один раз падает, а потом проходит. Запуск закончился с TEST SUCCEEDED. В сводке было 3 прогона для 2 тестов.

Значит, повтор делает нестабильный тест зелёным, и нестабильность прячется. Читайте result bundle после каждого запуска и отслеживайте, каким тестам понадобился повтор. Добавьте -test-repetition-relaunch-enabled YES, чтобы каждая попытка шла в новом процессе. Чтобы поймать нестабильный тест, используйте -run-tests-until-failure. Его нельзя сочетать с -retry-tests-on-failure.

7. Ограничьте зависшие тесты

-test-timeouts-enabled YES \
-default-test-execution-time-allowance 120 \
-maximum-test-execution-time-allowance 300

Добавьте эти флаги к команде тестов. Тогда UI-тест, который бесконечно ждёт элемент, упадёт после отведённого времени и не будет держать машину до таймаута CI.

8. Держите симуляторы чистыми между запусками

xcrun simctl shutdown "$UDID"
xcrun simctl erase "$UDID"
xcrun simctl delete unavailable

Erase сбрасывает содержимое и настройки симулятора. Delete unavailable удаляет устройства, которые текущий Xcode больше не поддерживает. Запускайте эту команду после каждого обновления Xcode.

Как это переживает перезагрузки

Созданные вами симуляторы сохраняются после перезагрузки. Вернуться должен агент CI, который запускает тесты. Запускайте его как LaunchAgent, включите автоматический вход и загружайте симулятор для CI в начале каждой задачи через bootstatus -b. Эта команда безопасна и для устройства, которое уже загружено.

Ошибки, на которые мы наткнулись, и их решения

  • Unable to find a device matching the provided destination specifier. Такого имени или ОС нет. Проверьте xcrun simctl list devices и список сред выполнения.
  • xcodebuild: error: Existing file at -resultBundlePath. Удаляйте старый bundle перед каждым запуском.
  • Unable to erase contents and settings in current state: Booted. Выключите симулятор, прежде чем стирать его.

Чем помогает выделенный Mac

UI-тесты тратят много времени до первого нажатия. Они ждут сборку, загрузку симулятора и установку приложения. Машина, которая всегда включена, держит DerivedData и загруженный симулятор наготове. В нашем тесте мы собирали iOS-приложение Wikipedia на Xcode 26.6. Задача после небольшого изменения заняла 27 секунд на прогретом M6. На свежем hosted раннере macos-26 у GitHub она заняла 269 секунд. У нашего M6 12 ядер CPU, поэтому два параллельных клона оставляют место для сборки.

Когда хватает hosted CI

Небольшой набор тестов на несколько pull request в день укладывается в поминутную оплату. При ставке GitHub $0.062 за минуту macOS (проверено в сентябре 2026 года) Mac за $139 окупается примерно после 2,242 минут в месяц. Ниже этого оставайтесь на hosted. Если нужно тестировать на многих физических iPhone, вам нужна облачная ферма устройств. Mac mini запускает симуляторы. Вашему параллельному набору тестов нужно больше 16 GB памяти? В наших конфигурациях на M5 Pro 48 или 64 GB. Но это предзаказ, так что заложите примерно неделю ожидания.

Руководство по пайплайну CI/CD для iOS показывает, где UI-тесты стоят в полном пайплайне. Калькулятор посчитает ваши цифры.

Частые вопросы

Есть ли у xcodebuild флаг для повтора упавших тестов?

+

Да. -retry-tests-on-failure перезапускает упавший тест до -test-iterations раз, а по умолчанию до 3. Его нельзя сочетать с -run-tests-until-failure.

Как запустить тесты XCUITest параллельно из командной строки?

+

Добавьте -parallel-testing-enabled YES и при желании -parallel-testing-worker-count. Xcode клонирует симулятор и распределяет классы тестов по клонам.

Может ли XCUITest работать на headless Mac?

+

Ему нужна сессия пользователя с выполненным входом, потому что UI-тесты управляют приложением Simulator. Запускайте агента CI как LaunchAgent и включите автоматический вход.

Как прочитать результаты xcodebuild test, не открывая Xcode?

+

Передайте -resultBundlePath, затем выполните для bundle команду xcrun xcresulttool get test-results summary --path. Она выведет числа в JSON.

Похожие руководства