Push-уведомления: чек-лист тестирования из 10 секций
Push-уведомления — один из самых недотестируемых классов фич. На QA-чек-листе обычно «пришёл / не пришёл», а реальных кейсов десятки: permission states, разные состояния приложения, deep linking, Do Not Disturb, локализация, тихие push, атрибуция в аналитике. Если хоть один из них сломан — вы теряете retention и не узнаете об этом.
Почему push сложно тестировать
- Цепочка из 4 систем: ваш бекенд → APNs / FCM → ОС девайса → ваше приложение. Любая может сломаться отдельно.
- Состояние приложения меняет поведение: foreground / background / killed — три разных кодовых пути обработки.
- OS-уровень фильтров: DND, Focus modes (iOS 15+), Notification channels (Android 8+), Low Power Mode — каждый может дропнуть пуш молча.
- Trigger часто асинхронный: между bekend-trigger и реальным push на экране может быть несколько секунд до минут. Тест «упало через минуту» — не баг и не норма, без замера.
1. Permission states
Главное что надо понимать — статусов больше двух. На iOS как минимум 5: notDetermined, denied, authorized, provisional (тихие пуши без пермишена), ephemeral (App Clip). Все надо проверить.
- Первый запуск: при cold start новый юзер видит prompt? Или мы сначала показываем soft-ask (свой объяснительный экран), а потом системный prompt? Apple предпочитает второе.
- Отказ от permission: после
Don't Allowприложение продолжает работать, ничего не падает. UI корректно отражает «notifications off». - Включение через Settings: юзер отказал → пошёл в Settings → включил → вернулся в приложение. Получит ли он push при следующем триггере? Часто нет — токен не зарегистрировался ретроактивно.
- Provisional authorization (iOS 12+): пуши приходят молча в Notification Center, юзер потом решает разрешить «громко» или не разрешить. Документация: Asking permission to use notifications.
- Notification channels на Android 8+: каждый канал может быть индивидуально отключён. Тестируйте каждый. Документация: Notification channels.
2. Состояния приложения
Три ветки кода, которые разработчики часто пишут раз и забывают. Их надо протестировать раздельно.
- Foreground: приложение открыто, юзер играет. На iOS пуш по умолчанию не показывается — приложение должно само решить (показать toast, in-app banner, или ничего). Тестируйте: пуш во время уровня → не ломает анимации, не воровствует фокус ввода.
- Background: приложение свернули, экран блокирован или нет. Пуш показывается в Notification Center. Тап → приложение открывается на правильном экране (см. п.5 deep linking).
- Killed (force-quit): юзер свайпнул приложение из task switcher. Пуш всё равно должен прийти. После тапа — приложение запускается с cold start, и должно знать, что было нажато (на iOS:
launchOptions[.remoteNotification]). Проверьте, что deep-link отрабатывает после полного запуска.
3. Типы уведомлений
- Alert / Banner: обычный визуальный пуш. Стандартный кейс.
- Silent push (content-available): пуш без UI, чтобы приложение обновило данные в фоне. Тестировать очень сложно — без логирования вы не узнаете, пришёл он или нет. iOS жёстко троттлит silent push (несколько в час). Проверка через Console.app + фильтр по subsystem.
- Time-sensitive (iOS 15+): пробивает Focus modes. Должны использоваться только для срочных уведомлений (Apple строго к этому относится — могут отклонить приложение).
- Critical alert (iOS): пробивает Mute и DND. Требует специальный entitlement от Apple. В играх не используется почти никогда.
- Provisional: тихие push, не требуют permission. Появляются в Notification Center, юзер решает.
4. Локализация и динамический контент
- Локализованный текст: если push формируется на сервере — он должен учитывать
localeюзера. Если на клиенте черезloc-key(iOS) — ключи должны быть во всех Localizable.strings. Тестируйте на каждом языке. - Переменные в тексте: «У вас 3 жизни» — что если жизней 0, 1, 21? Плюрализация работает? (см. предыдущий пост про локализацию).
- Эмодзи и спецсимволы: длинные имена, эмодзи, RTL-текст. Не обрезается, не ломает рендер.
- Длина: iOS Lock Screen показывает ~2 строки, Notification Center раскрывает. Длинный текст — корректно укладывается в обе формы.
5. Deep linking из push
Самая частая проблема: тап на push открывает приложение на главном экране, а не на нужном.
- Push «Скидка 50% на No-Ads» → тап → должен открыться Shop с подсвеченным офером. Не Home.
- Push «Награда за level-up» → тап → должна открыться награда. Не Home.
- Push приходит когда юзер уже в нужном экране → ничего не должно «прыгать». Только refresh данных.
- Push с deep-link на экран, требующий авторизации → если юзер вышел → корректный flow «сначала логин, потом deep-link».
- Push с deep-link на удалённый контент (event закончился, акция истекла) → понятный fallback на Home + информативное сообщение.
6. Фильтры на стороне ОС
Тестировать в каждом из этих режимов отдельно.
- Do Not Disturb (DND): пуш приходит молча в Notification Center, не звенит, не светится. Это норма. Time-sensitive пробивают DND.
- Focus modes (iOS 15+): юзер может разрешить уведомления только от Whitelisted apps. Если ваше приложение не в списке — пуш приходит в «summary» позже, не сразу. Тестируйте.
- Low Power Mode: iOS режет background fetch, silent push приходят с задержкой или дропаются.
- Doze mode (Android 6+): если девайс не двигался ~30 минут, неурочные push дропаются в maintenance windows. FCM-priority HIGH пробивает Doze.
- Аэроплан-режим: накопившиеся пуши приходят пачкой, когда сеть вернулась. Должны корректно показаться, не дублироваться.
7. Аналитика и атрибуция
Без событий push — это слепая ставка маркетинга. Должны логироваться:
push_received— приложение получило (даже если юзер не открыл).push_displayed— ОС показала пуш (важно: на iOS приложение этого не знает напрямую, нужны Notification Service Extensions для измерения).push_opened— юзер тапнул и приложение открылось.push_dismissed— юзер свайпнул не открывая (Android только).
Проверка: пуш с unique campaign_id → у себя в analytics видите 4 события с тем же campaign_id, attribution к follow-up действиям (покупке, level-up) сохраняется в течение N часов.
8. Edge cases из практики
- Юзер сменил язык OS после регистрации токена — пуши приходят на старом языке, потому что сервер хранит локаль с момента регистрации.
- Юзер сменил часовой пояс — пуш с relative time «через 1 час» приходит не вовремя. Сервер должен пересчитать.
- Юзер удалил приложение и переустановил — старый push token инвалидирован. На сервере висит «зомби» в БД. APNs / FCM возвращают
Unregistered— сервер должен это обработать и удалить токен. - Юзер дал permission, потом отозвал — токен у вас на сервере есть, но пуши не доходят. Без feedback вы будете слать в никуда.
- Несколько устройств у одного юзера — пуш должен идти на все? Только на активное? Когда отзывать токен старого устройства? Это бизнес-решение, но QA-сценарий — обязателен.
- Дубликаты — сервер случайно отправил один пуш дважды. Идемпотентность по
apns-collapse-id(iOS) /collapse_key(FCM) — оба сводят одинаковые пуши в один.
9. iOS-специфика
- APNs sandbox vs production: development-build бьёт в sandbox-APNs, App Store / TestFlight — в production. Они независимы — токен из sandbox не работает в production и наоборот. Самая частая «потеря пушей» при переходе с TestFlight на App Store.
- Badge count: число на иконке. Управляется отдельно через
badgeв payload или клиентским API. QA: после открытия пуша счётчик корректно сбрасывается. - Notification Service Extension: для модификации пуша на лету (декодирование, скачивание медиа, аналитика). Если есть — отдельно тестировать.
- Mutable content и медиа-аттачменты: пуш с картинкой / видео. На iOS — через NSE, лимит 5 MB.
10. Android-специфика
- Notification channels: каждый канал имеет свою importance (HIGH = heads-up, LOW = silent). Юзер может отключить отдельные каналы в Settings. Тестировать каждый канал отдельно.
- OEM-fragmentation: Xiaomi, Huawei, Samsung имеют свои системы аггресивной батареи. На Xiaomi приложение надо явно добавить в «Autostart whitelist», иначе пуши перестают приходить через несколько часов. Это не баг приложения — это OS. Но юзер не знает.
- FCM priority: NORMAL дропается в Doze, HIGH пробивает. Используйте HIGH только для срочных — Google наказывает аппы за злоупотребление.
- Background restrictions на Android 12+: можно вызвать сервис из push только в окне 10 секунд. Если ваш handler делает что-то долгое — упадёт.
QA-чек-лист на push-фичу
- Permission prompt появляется в нужный момент, отказ не ломает приложение
- Push приходит в foreground / background / killed — все три ветки
- Тап в каждом состоянии открывает правильный экран (deep link)
- Локализация: 2-3 «тяжёлых» языка, плюрализация работает
- DND / Focus mode / Low Power: пуш приходит соответственно настройкам ОС
- Notification channels на Android: каждый отключаем-включаем индивидуально
- Badge count корректно меняется и сбрасывается
- Аналитика 4 события: received / displayed / opened / dismissed
- Attribution к follow-up действиям сохраняется N часов
- Удаление и переустановка приложения — старый токен инвалидируется на сервере
- Дубликаты сворачиваются через collapse-id
- Edge: смена языка, часового пояса, multi-device
Тулзы для тестирования
- APNs Tester / Pusher / NWPusher — десктоп-приложения для отправки тестовых push на iOS без сервера.
- Firebase Console Cloud Messaging → Send test message — для FCM/Android, можно отправить пуш на конкретный токен.
- Push Notification Tester — VS Code / JetBrains plugins для отправки прямо из IDE.
- Charles / Proxyman — поймать запрос регистрации токена, посмотреть что ваш бекенд отправляет в APNs/FCM.
- Console.app на Mac + USB-iPhone: фильтр
subsystem:com.apple.cfnetworkпокажет даже silent push.