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

Автоматическая проверка push-уведомлений iOS через simctl

Автоматическая проверка push-уведомлений iOS через simctl

Удалённый конвейер уже создал устанавливаемую сборку iOS-приложения, но у команды нет постоянно подключённого тестового устройства. В такой ситуации чаще всего остаются незамеченными не ошибки компиляции, а проблемы во взаимодействии разрешений на уведомления, показа на переднем плане, параметров глубоких ссылок и расширения обработки уведомлений. Команда simctl push позволяет напрямую передавать в iOS Simulator полезную нагрузку в формате APNs и хорошо подходит для быстрой и воспроизводимой проверки перед коммитом на облачном Mac.

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

Сначала определите границы проверки

simctl push проверяет, как уже собранное приложение обрабатывает полезную нагрузку уведомления. С его помощью можно протестировать разбор содержимого уведомления, вызовы методов делегата на переднем и заднем плане, кнопки категорий, маршрутизацию глубоких ссылок и результат работы Notification Service Extension.

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

Успешное выполнение команды означает только завершение локальной инъекции, а не получение уведомления пользователем. Результат проверки должен определяться по интерфейсу, журналам приложения и результату маршрутизации.

Перед началом убедитесь, что в приложении объявлена возможность работы с уведомлениями, а на тестовом пути уже предоставлено разрешение. simctl push не нажимает за приложение кнопки в системном запросе разрешения. Первичную выдачу разрешения можно автоматизировать с помощью UI-теста или заранее выполнить на выделенном тестовом симуляторе, но нельзя полагаться на состояние симулятора, который используется длительное время.

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

Не используйте booted напрямую в параллельных заданиях. Если одновременно запущено несколько симуляторов, полезная нагрузка может попасть в другое задание. Для каждого задания указывайте конкретный UDID, а путь к приложению, Bundle ID и каталог артефактов передавайте как входные данные.

set -euo pipefail

: "${SIMULATOR_UDID:?Set SIMULATOR_UDID}"
: "${APP_PATH:?Set APP_PATH}"
: "${BUNDLE_ID:?Set BUNDLE_ID}"

ARTIFACTS="${ARTIFACTS:-$PWD/.ci/push-check}"
mkdir -p "$ARTIFACTS"

xcrun simctl bootstatus "$SIMULATOR_UDID" -b
xcrun simctl install "$SIMULATOR_UDID" "$APP_PATH"
xcrun simctl launch "$SIMULATOR_UDID" "$BUNDLE_ID"
xcrun simctl get_app_container "$SIMULATOR_UDID" "$BUNDLE_ID"

bootstatus -b дожидается фактического завершения запуска системы и поэтому надёжнее фиксированной паузы в несколько секунд. get_app_container служит простой проверкой установки: если Bundle ID указан неверно или приложение не установлено, задание завершится с ошибкой ещё до отправки уведомления.

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

Сформируйте и проверьте полезную нагрузку APNs

Файл полезной нагрузки должен быть в формате JSON. Добавьте доступный для поиска тестовый идентификатор, чтобы связать снимки экрана и журналы приложения с конкретным заданием конвейера.

PAYLOAD="$ARTIFACTS/foreground.apns"
TRACE_ID="push-check-${CI_RUN_ID:-local}"

cat > "$PAYLOAD" <<JSON
{
  "Simulator Target Bundle": "$BUNDLE_ID",
  "aps": {
    "alert": {
      "title": "Build completed",
      "body": "Open the result for details"
    },
    "sound": "default",
    "badge": 1,
    "category": "BUILD_RESULT"
  },
  "route": "build/result",
  "trace_id": "$TRACE_ID"
}
JSON

plutil -lint "$PAYLOAD"
PAYLOAD_BYTES="$(wc -c < "$PAYLOAD" | tr -d ' ')"
test "$PAYLOAD_BYTES" -le 4096
xcrun simctl push "$SIMULATOR_UDID" "$BUNDLE_ID" "$PAYLOAD"

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

Если используется расширение обработки уведомлений, добавьте "mutable-content": 1 и настройте запись структурированных сообщений расширения в унифицированный журнал. Время выполнения расширения и его принудительное завершение системой всё равно следует учитывать как возможные сценарии отказа: нельзя рассчитывать, что загрузка или изменение содержимого обязательно завершатся.

Составьте матрицу проверок для состояний приложения

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

Сценарий Действие Обязательные проверки
Передний план Запустить приложение и передать нагрузку Вызов делегата, сообщение внутри приложения, дублирующиеся события
Задний план Перейти на главный экран и передать нагрузку Содержимое баннера, значок, маршрут после нажатия
Не запущено Завершить приложение и передать нагрузку Точка входа холодного запуска, разбор параметров, целевой экран
Расширение содержимого Передать нагрузку с mutable-content Журнал расширения, изменённое содержимое, резервный сценарий при тайм-ауте
Некорректные поля Удалить маршрут или указать неверный тип поля Безопасная деградация, отсутствие сбоя, диагностические журналы

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

xcrun simctl io "$SIMULATOR_UDID" screenshot \
  "$ARTIFACTS/notification.png"

xcrun simctl spawn "$SIMULATOR_UDID" log show \
  --last 2m \
  --style compact \
  --predicate "process == '$BUNDLE_ID'" \
  > "$ARTIFACTS/application.log"

Имя процесса не всегда совпадает с Bundle ID. Надёжнее использовать в приложении и расширении постоянный subsystem журналирования, а затем выполнять запрос по этому subsystem. Записывайте в журналы только тестовый идентификатор, состояние и тип ошибки — без полного текста уведомления, токенов и пользовательских данных.

Зафиксируйте условия отказа в конвейере

Надёжное задание проверки уведомлений не должно ограничиваться выполнением shell-команд. Оно также должно проверять наличие артефактов, отсутствие сбоя приложения и регистрацию ожидаемого маршрута. В тестовой сборке приложение может записывать обработанный trace_id и результат в отдельный файл. Затем контейнер можно найти через simctl get_app_container и прочитать этот файл. Такой тестовый интерфейс следует включать только во внутренних конфигурациях сборки и обязательно отключать в конфигурации выпуска.

К типичным ошибочным выводам относятся: повторное использование симулятора с уже выданным разрешением, из-за чего не проверяется первичный запрос; применение booted, приводящее к смешению параллельных заданий; трактовка отсутствия баннера на переднем плане как сбоя инъекции; проверка только снимка экрана без проверки маршрута после нажатия; слишком широкий диапазон поиска в журналах, захватывающий записи предыдущего задания.

Перед коммитом завершите проверку в следующем порядке:

  1. Зафиксируйте версии Xcode и среды выполнения, модель устройства и UDID симулятора.
  2. Установите артефакт текущей сборки, не используя старый контейнер приложения.
  3. Проверьте синтаксис JSON, размер в байтах и наличие обязательных полей.
  4. Отдельно выполните сценарии для переднего плана, заднего плана, незапущенного приложения и некорректной полезной нагрузки.
  5. Сохраните полезную нагрузку, снимки экрана, журналы приложения и уникальный тестовый идентификатор.
  6. Отчитывайтесь о проверке на симуляторе отдельно от сквозного тестирования реальной доставки через APNs.

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

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

Означает ли успешный simctl push, что реальная доставка APNs работает?

Нет. Команда подтверждает только локальную инъекцию в Simulator. Проверку серверной авторизации, токена устройства, сети и ограничений APNs проводят отдельно на контролируемом устройстве.

Почему после успешной команды не появляется баннер?

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

Что использовать в CI: booted или конкретный UDID?

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

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

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

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

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