Инженерное руководство MiniRent

Повторяемые тесты разрешений iOS через simctl privacy

Повторяемые тесты разрешений iOS через simctl privacy

Один и тот же набор UI-тестов iOS может без ошибок проходить на машине разработчика, но периодически зависать на диалоге разрешений после переноса на облачный Mac. Обычно причина не в случайном сбое бизнес-логики, а в том, что симулятор сохранил состояние разрешений TCC от предыдущего задания. Если кто-то уже нажал «Разрешить», последующие тесты не увидят первоначальный запрос. Если было выбрано «Не разрешать», сценарии, которым нужны контакты или фотографии, сразу перейдут в ветку отказа. Стабильность достигается не более частыми повторами, а явным заданием исходного состояния разрешений перед каждым тестом.

Рассматривайте состояние разрешений как входные данные теста

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

Целевое состояние Действие simctl Ожидаемое поведение
Решение не принято reset Приложение запрашивает доступ, система показывает диалог разрешения
Доступ разрешён grant Функция продолжает работу без повторного запроса
Доступ запрещён revoke Приложение показывает описание ограниченного режима или предлагает перейти в настройки

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

xcrun simctl privacy help
xcrun simctl list devices available

К распространённым службам относятся contacts, photos, photos-add, location, microphone и calendar. Тестовый скрипт должен ориентироваться на справку той версии simctl, которая входит в текущую поставку Xcode.

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

Подготовьте отдельный симулятор и приложение

В CI не следует использовать booted, позволяя ему указывать на произвольное запущенное устройство. Параллельные задания могут выбрать один и тот же симулятор, из-за чего установка, запуск и изменение разрешений будут перезаписывать результаты друг друга. Надёжнее назначить заданию конкретный UDID и передавать его скрипту как параметр.

Чтобы grant или revoke применялись к реальному Bundle Identifier, приложение необходимо сначала установить. Не следует выводить Bundle Identifier из имени Scheme: его можно прочитать непосредственно из результата сборки.

APP_PATH="$1"
UDID="$2"
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" \
  "$APP_PATH/Info.plist")

xcrun simctl boot "$UDID" 2>/dev/null || :
xcrun simctl bootstatus "$UDID" -b
xcrun simctl install "$UDID" "$APP_PATH"
printf '%s
' "$BUNDLE_ID"

Если тестируется расширение, у него есть собственный Bundle Identifier. Прочитайте Info.plist расширения отдельно и не используйте идентификатор основного приложения во всех командах управления разрешениями.

Зафиксируйте три исходных состояния с помощью скрипта

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

set -euo pipefail

UDID="$1"
BUNDLE_ID="$2"
SERVICE="$3"
STATE="$4"

xcrun simctl terminate "$UDID" "$BUNDLE_ID" 2>/dev/null || :

case "$STATE" in
  undecided)
    xcrun simctl privacy "$UDID" reset "$SERVICE" "$BUNDLE_ID"
    ;;
  authorized)
    xcrun simctl privacy "$UDID" grant "$SERVICE" "$BUNDLE_ID"
    ;;
  denied)
    xcrun simctl privacy "$UDID" revoke "$SERVICE" "$BUNDLE_ID"
    ;;
  *)
    printf 'Unknown permission state: %s
' "$STATE" >&2
    exit 64
    ;;
esac

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

./set-permission.sh "$UDID" "$BUNDLE_ID" contacts authorized

xcodebuild test \
  -scheme PermissionTests \
  -destination "platform=iOS Simulator,id=$UDID" \
  -only-testing:PermissionUITests/ContactsAuthorizedTests \
  -resultBundlePath Artifacts/ContactsAuthorized.xcresult

reset возвращает состояние «решение не принято», а не устанавливает отказ. Если бизнес-логика обрабатывает эти два состояния одинаково, тест должен явно выявить эту проблему проектирования.

Разделяйте тестирование системного диалога и бизнес-веток

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

Сценарий первоначального запроса

Сначала выполните reset, затем запустите приложение и инициируйте запрос разрешения. Тест должен проверять только появление диалога и переход приложения на ожидаемый экран после выбора пользователя. Если требуется взаимодействие с системным интерфейсом, используйте отдельный объект системного приложения и не полагайтесь на фиксированные координаты.

Сценарии разрешённого и запрещённого доступа

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

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

Изоляция, сбор данных и диагностика сбоев

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

При сбое сохраняйте как минимум .xcresult, журналы тестов, системные журналы симулятора и текущий список устройств. Если ожидаемое состояние не применилось, проверяйте следующие пункты по порядку:

  1. Получен ли Bundle Identifier из фактически установленного приложения или расширения.
  2. Поддерживается ли имя службы текущей средой выполнения.
  3. Было ли приложение завершено до изменения состояния.
  4. Совпадает ли целевой UDID с устройством, которое использует xcodebuild.
  5. Не работает ли с тем же симулятором другое параллельное задание.
  6. Не кэширует ли тестовый код прежний результат авторизации.

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

При выполнении таких заданий на облачных Mac от MiniRent модель и узел влияют только на размещение задания. Тестовый скрипт всё равно должен полностью описывать среду через UDID, Bundle Identifier, имя службы и целевое состояние, а доступные конфигурации следует проверять в консоли. Тогда один и тот же набор регрессионных тестов разрешений можно будет как воспроизводить в краткосрочных отладочных заданиях, так и стабильно выполнять в непрерывной интеграции.

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

Может ли simctl privacy заменить все тесты системного запроса?

Нет. Команда хорошо подготавливает состояние, но текст настоящего диалога, его кнопки и возврат в приложение следует проверять отдельным небольшим набором UI-тестов.

Нужно ли очищать симулятор перед каждым тестом разрешений?

Обычно нет. Завершите приложение и выполните reset, grant или revoke для нужной службы и идентификатора пакета.

Выделенное физическое устройство

Запустите следующую задачу разработки или сборки на облачном Mac mini

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

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