Инженерные практики

Как устранить нестабильные сбои XCUITest на облачном Mac

Как устранить нестабильные сбои XCUITest на облачном Mac

Один и тот же набор XCUITest может стабильно проходить локально, но после переноса на облачный Mac время от времени не находить кнопку, завершаться по тайм-ауту или зависать на экране запуска. Обычно дело не только в том, что «машина медленная». Чаще причиной становятся меняющаяся цель запуска, состояние от предыдущего теста, ожидания фиксированной длительности и отсутствие достаточных диагностических данных после сбоя. Устранять нестабильность следует поэтапно: сначала зафиксировать входные параметры, затем исключить неявные зависимости и лишь после этого рассматривать повторные запуски.

Зафиксируйте входные параметры каждого запуска

Явно задайте в команде Xcode, план тестирования, целевой симулятор и каталог сборки, не полагаясь на параметры, оставшиеся выбранными в графическом интерфейсе. Облачный Mac может длительное время выполнять разные задачи, а один рабочий каталог может использоваться несколькими конвейерами. Поэтому каждому запуску нужны отдельные DerivedData и пакет результатов.

RUN_ID="${CI_RUN_ID:-local-$(date +%s)}"
ARTIFACTS="$PWD/artifacts/$RUN_ID"
DERIVED="$PWD/.derived/$RUN_ID"

mkdir -p "$ARTIFACTS" "$DERIVED"

xcodebuild test \
  -workspace Sample.xcworkspace \
  -scheme SampleUITests \
  -testPlan Smoke \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -derivedDataPath "$DERIVED" \
  -resultBundlePath "$ARTIFACTS/Smoke.xcresult" \
  -parallel-testing-enabled NO

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

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

Перед выполнением сохраните вывод xcodebuild -version, а командой xcrun simctl list devices available убедитесь, что целевое устройство действительно доступно. Не указывайте только имя устройства без версии системы и не позволяйте разным Runner использовать один каталог DerivedData.

Замените фиксированные паузы ожиданием наблюдаемого состояния

Thread.sleep выглядит простым решением, но фактически подменяет условие «экран готов» условием «прошло несколько секунд». При быстром ответе сети это напрасно увеличивает время теста, а при более медленной анимации или дисковой операции ожидание заканчивается слишком рано.

let app = XCUIApplication()
app.launchArguments += ["--ui-testing", "--reset-state"]
app.launch()

let continueButton = app.buttons["onboarding.continue"]
let ready = continueButton.waitForExistence(timeout: 12)

XCTAssertTrue(ready, "Continue button did not appear")
XCTAssertTrue(continueButton.isHittable)
continueButton.tap()

Для элементов следует использовать стабильные accessibility identifier, а не локализованный текст или положение в иерархии представлений. Если кнопка уже существует, но недоступна для нажатия, проверьте перекрывающие слои, анимацию и положение прокрутки, а не просто увеличивайте тайм-аут.

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

Начинайте каждый тест с известного состояния

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

Разделите обработку состояния по следующим уровням:

Тип состояния Рекомендуемый подход Нерекомендуемый подход
Настройки приложения Запускать специальный тестовый сброс через аргументы запуска Считать, что после установки состояние всегда пустое
Локальная база данных Импортировать фиксированные тестовые данные или пересоздавать тестовую базу Зависеть от данных, записанных предыдущим тестом
Серверные данные Использовать уникальное пространство имён для текущего запуска Совместно использовать постоянные записи в нескольких задачах
Системные разрешения Единообразно подготовить и проверить перед запуском набора Настраивать ситуативно внутри произвольного теста
Процесс приложения Явно завершать после каждого теста Считать, что после сбоя приложение восстановится автоматически

При очистке на уровне набора можно завершать приложение. Удалять его следует только тогда, когда действительно проверяется сценарий первого запуска после установки. Частый полный сброс симулятора увеличивает продолжительность тестирования и может замаскировать проблему окружения как ситуацию «после очистки всё прошло».

xcrun simctl terminate booted "$APP_BUNDLE_ID" 2>/dev/null || true

Используйте повторный запуск как инструмент классификации

Автоматический повторный запуск не должен заменять критерий успешного прохождения. Разумная стратегия — после первого сбоя повторить тест только один раз и отдельно сохранить attempt-1.xcresult и attempt-2.xcresult. Если оба запуска завершились с ошибкой, проблему прежде всего следует считать воспроизводимым дефектом. Если первый запуск не прошёл, а второй прошёл, тест нужно пометить как нестабильный и продолжить проверку условий ожидания, общего состояния и конкуренции за ресурсы.

Не сохраняйте только итоговый код завершения

При каждом сбое собирайте как минимум следующие данные:

  1. Отдельный пакет результатов и журналы тестирования;
  2. Снимок экрана и иерархию интерфейса на шаге сбоя;
  3. Версию Xcode, целевой симулятор и аргументы запуска;
  4. Идентификатор запуска, имя теста, время начала и окончания;
  5. Статус завершения приложения и соответствующие фрагменты системных журналов.

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

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

Сначала многократно запустите целевой тест с параметром -only-testing, чтобы исключить посторонние переменные. Затем выполните весь класс, к которому относится тест, и только после этого вернитесь к полному набору. Если сбой возникает только в полном наборе, это обычно указывает на зависимость от порядка выполнения, общие данные или конкуренцию за ресурсы.

xcodebuild test \
  -workspace Sample.xcworkspace \
  -scheme SampleUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -only-testing:SampleUITests/CheckoutTests/testSubmit \
  -parallel-testing-enabled NO

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

Конечная цель — не скрыть частоту сбоев за повторными запусками, а обеспечить для каждого запуска определённые входные параметры, явные условия ожидания, изолированное состояние и полные диагностические данные. Только при соблюдении этих четырёх условий XCUITest на облачном Mac перестанут быть тестами, которые «иногда проходят», и станут надёжным инженерным сигналом для принятия решений о слиянии изменений и выпуске версии.

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

Нужно ли автоматически повторять упавший XCUITest?

Один повтор полезен для классификации, но исходный сбой нельзя скрывать. Сохраните оба пакета результатов, а тесты, проходящие только со второй попытки, внесите в список нестабильных.

Почему в UI-тестах не стоит использовать фиксированный sleep?

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

Выделенный физический Mac mini

Выберите облачный Mac для следующего конвейера разработки

Сравните три фиксированные конфигурации и четыре узла в Азии, а затем выберите срок аренды с учётом реальной нагрузки. Каждая аренда включает отдельную физическую машину, а не виртуальную.

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