mobile-qachecklists

Push notifications: a 10-section testing checklist

Push notifications are one of the most under-tested features in mobile apps. A QA checklist usually says “arrived / not arrived”. The reality is dozens of cases: permission states, different app states, deep linking, Do Not Disturb, localization, silent push, attribution in analytics. If any single one is broken — you lose retention and won’t know about it.

Why push is hard to test

  • A chain of 4 systems: your backend → APNs / FCM → device OS → your app. Any one can fail independently.
  • App state changes behaviour: foreground / background / killed — three different code paths for handling.
  • OS-level filters: DND, Focus modes (iOS 15+), Notification channels (Android 8+), Low Power Mode — each can silently drop a push.
  • Trigger is often asynchronous: between the backend trigger and a push actually appearing on screen, seconds or minutes pass. “Fell after a minute” is neither a bug nor normal unless measured.

1. Permission states

The main thing to understand — there are more than two statuses. On iOS at least 5: notDetermined, denied, authorized, provisional (silent without permission), ephemeral (App Clip). All must be checked.

  • First launch: on cold start a new user sees the prompt? Or do we first show a soft-ask (our own explainer screen), then the system prompt? Apple prefers the latter.
  • Permission declined: after Don't Allow the app keeps working, doesn’t crash. UI correctly reflects “notifications off”.
  • Enabling via Settings: user declined → went to Settings → enabled → returned to the app. Will they receive push on the next trigger? Often not — the token never registered retroactively.
  • Provisional authorization (iOS 12+): pushes arrive silently in Notification Center; the user later decides to allow “loudly” or deny. Docs: Asking permission to use notifications.
  • Notification channels on Android 8+: each channel can be individually disabled. Test each. Docs: Notification channels.

2. App states

Three code branches developers often write once and forget. Test them separately.

  • Foreground: app is open, user is playing. On iOS by default the push is not shown — the app must decide itself (toast, in-app banner, or nothing). Test: push during a level → doesn’t break animations, doesn’t steal input focus.
  • Background: app is backgrounded, screen locked or not. Push shows in Notification Center. Tap → app opens at the correct screen (see #5 deep linking).
  • Killed (force-quit): user swiped the app out of task switcher. Push still arrives. After tap — the app starts cold, and should know what was tapped (on iOS: launchOptions[.remoteNotification]). Check that the deep-link works after a full cold start.

3. Notification types

  • Alert / Banner: regular visual push. Standard case.
  • Silent push (content-available): a push with no UI, so the app updates data in the background. Very hard to test — without logging you don’t know it arrived. iOS throttles silent push hard (a few per hour). Verify via Console.app + filter by subsystem.
  • Time-sensitive (iOS 15+): pierces Focus modes. Should be reserved for genuinely urgent notifications (Apple is strict — they may reject the app).
  • Critical alert (iOS): pierces Mute and DND. Requires a special entitlement from Apple. Almost never used in games.
  • Provisional: silent pushes that don’t need permission. Appear in Notification Center, user decides.

4. Localization and dynamic content

  • Localized text: if the push is built on the server — it must respect the user’s locale. If built on the client via loc-key (iOS) — keys must exist in every Localizable.strings. Test in every language.
  • Variables in text: “You have 3 lives” — what about 0, 1, 21 lives? Does pluralization work? (see the previous post on localization).
  • Emoji and special characters: long names, emoji, RTL text. Doesn’t truncate, doesn’t break rendering.
  • Length: iOS Lock Screen shows ~2 lines, Notification Center expands. Long text wraps correctly in both views.

5. Deep linking from push

The most common problem: tap on push opens the app at the home screen instead of the relevant one.

  • Push “50% off No-Ads” → tap → must open Shop with the offer highlighted. Not Home.
  • Push “Level-up reward” → tap → must open the reward screen. Not Home.
  • Push arrives while the user is already on the relevant screen → nothing should “jump”. Refresh data only.
  • Push with a deep-link to a screen that requires auth → if signed out → proper “log in first, then deep-link” flow.
  • Push with a deep-link to expired content (event over, sale ended) → graceful fallback to Home + informative message.

6. OS-level filters

Test in each of these modes separately.

  • Do Not Disturb (DND): push arrives silently in Notification Center, doesn’t ring, doesn’t flash. This is normal. Time-sensitive pierce DND.
  • Focus modes (iOS 15+): user can allow notifications only from whitelisted apps. If your app isn’t on the list — push arrives in the “summary” later, not immediately. Test.
  • Low Power Mode: iOS throttles background fetch, silent push arrives delayed or is dropped.
  • Doze mode (Android 6+): if the device hasn’t moved for ~30 minutes, non-urgent pushes are dropped into maintenance windows. FCM-priority HIGH pierces Doze.
  • Airplane mode: accumulated pushes arrive as a batch when network returns. Must display correctly, not duplicate.

7. Analytics and attribution

Without push events, push is a blind marketing bet. These must be logged:

  • push_received — the app received it (even if the user didn’t open it).
  • push_displayed — the OS displayed it (important: on iOS the app doesn’t know this directly, you need Notification Service Extensions to measure).
  • push_opened — user tapped and the app opened.
  • push_dismissed — user swiped without opening (Android only).

Verification: push with a unique campaign_id → in your analytics you see 4 events with the same campaign_id, and attribution to follow-up actions (purchase, level-up) persists for N hours.

8. Edge cases from production

  • User changed OS language after registering the token — pushes arrive in the old language because the server stored the locale at registration time.
  • User changed time zone — a push with relative time “in 1 hour” arrives off-schedule. The server must recompute.
  • User deleted and reinstalled the app — old push token invalidated. Your server has a “zombie” in the DB. APNs / FCM return Unregistered — the server must handle this and delete the token.
  • User granted permission, then revoked it — your server still has the token, but pushes don’t reach. Without feedback you send into the void.
  • Multi-device user — should push go to all devices? Only the active one? When to invalidate the old device’s token? It’s a business decision, but the QA scenario is mandatory.
  • Duplicates — server accidentally sent the same push twice. Idempotency via apns-collapse-id (iOS) / collapse_key (FCM) — both collapse identical pushes into one.

9. iOS specifics

  • APNs sandbox vs production: development builds hit sandbox APNs; App Store / TestFlight hit production. They’re independent — a sandbox token doesn’t work in production and vice versa. The most common “lost pushes” issue when shipping from TestFlight to App Store.
  • Badge count: the number on the icon. Managed separately via badge in the payload or a client API. QA: after opening a push, the counter resets correctly.
  • Notification Service Extension: to modify the push on the fly (decoding, fetching media, analytics). If you have one — test it separately.
  • Mutable content and media attachments: push with image / video. iOS — via NSE, 5 MB limit.

10. Android specifics

  • Notification channels: each channel has its own importance (HIGH = heads-up, LOW = silent). User can disable channels individually in Settings. Test each channel separately.
  • OEM fragmentation: Xiaomi, Huawei, Samsung have their own aggressive battery systems. On Xiaomi you must explicitly add the app to “Autostart whitelist” or pushes stop arriving after a few hours. Not a bug in your app — it’s the OS. But the user doesn’t know.
  • FCM priority: NORMAL is dropped in Doze, HIGH pierces. Use HIGH only for urgent — Google penalizes apps that abuse it.
  • Background restrictions on Android 12+: you have only 10 seconds to start a service from a push handler. If your handler does long work — it’ll crash.

QA checklist for a push feature

  • Permission prompt appears at the right moment, decline doesn’t break the app
  • Push arrives in foreground / background / killed — all three branches
  • Tap in each state opens the correct screen (deep link)
  • Localization: 2-3 “heavy” languages, pluralization works
  • DND / Focus mode / Low Power: push respects OS settings
  • Notification channels on Android: each one disabled-enabled individually
  • Badge count changes and resets correctly
  • Analytics: 4 events received / displayed / opened / dismissed
  • Attribution to follow-up actions persists for N hours
  • Delete + reinstall — old token invalidated on the server
  • Duplicates are collapsed via collapse-id
  • Edge: language change, timezone change, multi-device

Tools for testing

  • APNs Tester / Pusher / NWPusher — desktop apps for sending test pushes to iOS without a server.
  • Firebase Console Cloud Messaging → Send test message — for FCM/Android, can target a specific token.
  • Push Notification Tester — VS Code / JetBrains plugins to send right from your IDE.
  • Charles / Proxyman — capture the token-registration request, see what your backend sends to APNs/FCM.
  • Console.app on Mac + USB iPhone: filter subsystem:com.apple.cfnetwork shows even silent pushes.