How to Automate Push Notifications Testing (Step-by-Step)

How to Automate Push Notifications Testing (Step-by-Step) begins with a clear definition of what you intend to verify and ends with a repeatable CI pipeline. Push notifications are a critical touchpoi

April 27, 2026 · 17 min read · How-To Guides

How to Automate Push Notifications Testing (Step-by-Step) begins with a clear definition of what you intend to verify and ends with a repeatable CI pipeline. Push notifications are a critical touchpoint for user engagement, yet they are notoriously flaky to test manually because they depend on timing, network state, device permissions, and backend triggers. Automating this verification gives you fast feedback on regressions, lets you exercise edge‑case scenarios (e.g., notification while the app is in the background, tapped action handling, silent pushes), and frees QA to focus on exploratory work. In the following guide you will find a concrete test matrix, a framework comparison, step‑by‑step instructions for building reliable tests, handling waits and flakiness, managing test data, wiring everything into CI, and reporting results. Each section includes real code snippets you can copy into your repo, and we show how an autonomous exploration tool like SUSA can bootstrap the effort without writing a single line of test code.

How to Automate Push Notifications Testing (Step-by-Step): Overview and When It Pays Off

Before you write a single line of code, decide which aspects of push notifications merit automation. Not every notification scenario needs a script; focus on those that are high‑risk, repeatable, and costly to verify manually.

Test Matrix: Manual vs Automated Coverage

Notification AspectManual FeasibilityAutomation ROIRecommended Approach
Delivery timing (latency < 2 s)Low (requires precise stopwatch)HighAutomated with timestamp logging
Content correctness (title, body, image)Medium (visual check)MediumAutomated via UI inspection or API validation
Action button tap (deep link)Medium (requires device interaction)HighAutomated UI tap + navigation assert
Silent / background push (no UI)Very low (no visible cue)HighAutomated via backend receipt verification or silent‑push handling code
Permission prompts (allow/deny)Medium (dialog handling)MediumAutomated if dialog appears predictably
Do‑Not‑Disturb / battery‑optimization impactLow (device‑specific settings)LowMostly manual or device‑farm matrix
Localization of notification textMedium (language switch)MediumAutomated with locale‑specific resource checks
High‑volume burst (10+ notifications)Very low (tedious)HighAutomated with loop and rate‑limiting checks

From the matrix, prioritize automation for delivery timing, content verification, action handling, silent pushes, and burst scenarios. Manual effort remains valuable for exploratory UI quirks, device‑specific battery optimizations, and ad‑hoc permission flows.

When Automation Pays Off

  1. Regression safety – Every release that touches the push‑notification pipeline (FCM/APNs config, payload builder, foreground service) should run the automated suite.
  2. Cross‑device confidence – Running the same script on a fleet of emulators or real devices surfaces timing differences that manual testers miss.
  3. Performance baselines – By logging timestamps from receipt to UI display you can detect regressions in latency introduced by SDK updates.
  4. Release gating – Integrate the suite into your pull‑request workflow; a failing notification test blocks merge until the root cause is fixed.
  5. Scalability – Adding new notification types (e.g., in‑app messaging, promotional banners) only requires extending the data‑driven test suite, not writing fresh manual test cases.

If your team ships push‑notification changes more than once per sprint, automation will save hours each cycle. For low‑frequency changes, a lightweight smoke test may suffice.

How to Automate Push Notifications Testing (Step-by-Step): Choosing the Right Framework

The choice of framework hinges on three factors: target platform (Android, iOS, web), language preference of your team, and existing test infrastructure. Below we compare the most common options.

Tool Comparison Table

FrameworkPlatformLanguagePush Notification SupportSetup ComplexityCommunity MaturityTypical Use Case
Appium (Android)AndroidJava, JS, Python, RubyCan intercept system notifications via UIAutomator2; can read notification shadeMedium (requires Android SDK, emulator/device)HighNative Android apps
XCUITestiOSSwift/Obj‑CAccess to UNUserNotificationCenter; can simulate taps on bannersLow (bundled with Xcode)HighNative iOS apps
PlaywrightWeb (Chromium/Firefox/WebKit)JS/TS, Python, Java, .NETCan grant notification permission, listen to notification events, verify payloadLow (single binary)GrowingPWAs, SPA, web push
Firebase Test Lab + Robo ScriptAndroid/iOSJava/Kotlin/SwiftCan trigger FCM via test‑lab hooks; limited UI verificationMedium (requires Firebase project)HighLarge device farm testing
EspressoAndroidJava/KotlinNo direct notification shade access; relies on IdlingResource + BroadcastReceiverLow (if already using Espresso)HighUnit‑style UI tests within Android test source set
DetoxReact NativeJSCan simulate push via react-native-push-notification mock; works with JestMediumMediumRN apps

For most teams, Appium (Android) combined with Playwright (web) offers the broadest coverage with a single language stack (JavaScript/TypeScript). If you already have an Espresso or XCUITest suite, extend those rather than introducing a new framework.

Decision Flow

  1. Is your app purely native Android? → Start with Appium + UIAutomator2.
  2. Do you also need iOS coverage? → Add XCUITest in parallel; keep test data JSON shared.
  3. Is a significant portion of your audience using a PWA or web push? → Add Playwright tests that run against the same staging URL.
  4. Do you need to test on dozens of device models quickly? → Use Firebase Test Lab or AWS Device Farm as the execution backend, invoking Appium tests via their CLI.

Once you pick the stack, scaffold a minimal project:


# Example for TypeScript + Appium
npm init -y
npm install --save-dev appium typescript ts-node @types/node @types/appium
npx tsc --init --rootDir src --outDir build --esModuleInterop true --strict true

Add a wdio.conf.js (WebdriverIO) or jest.config.js if you prefer Jest as the test runner. The exact runner is less important than having a reliable way to start/stop the Appium server and to capture logs.

How to Automate Push Notifications Testing (Step-by-Step): Building Stable Tests

Stability hinges on three pillars: deterministic test data, robust locators, and explicit synchronization. Below we walk through a complete Android example using Appium (Java) that verifies a push notification appears, contains the expected payload, and launches the correct deep‑link when tapped.

1. Test Data Preparation

Store notification payloads in JSON files under src/test/resources/notifications/. Each file mimics the exact FCM payload your backend sends.


// welcome_push.json
{
  "to": "/topics/all",
  "notification": {
    "title": "Welcome aboard!",
    "body": "Your journey starts now.",
    "icon": "ic_launcher",
    "click_action": "OPEN_WELCOME_SCREEN"
  },
  "data": {
    "screen": "welcome",
    "promo_code": "WELCOME10"
  }
}

A utility class reads these files and builds a RemoteMessage‑like object that the test will push via adb shell cmd notification post or via a mock FCM server.


public class NotificationLoader {
    public static JsonNode load(String filename) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.readTree(
            Resources.getResource(filename).openStream()
        );
    }
}

2. Launching the App and Granting Permissions

Before each test, clear app data, grant notification permission, and force‑stop the app to ensure a clean slate.


@BeforeEach
void setUp() throws Exception {
    // Reset app state
    driver.executeScript("mobile: shell", ImmutableMap.of(
        "command", "pm clear com.example.app"
    ));
    // Grant notification permission (Android 13+)
    driver.executeScript("mobile: shell", ImmutableMap.of(
        "command", "appops set com.example.app POST_NOTIFICATIONS allow"
    ));
    // Start the app
    driver.launchApp();
}

3. Sending a Push Notification

For Android, the simplest way to inject a notification without a real backend is to use the adb command cmd notification post. Wrap it in a helper that takes the JSON payload.


public void sendPushNotification(JsonNode payload) throws IOException, InterruptedException {
    String title = payload.path("notification").path("title").asText();
    String body  = payload.path("notification").path("body").asText();
    String icon  = payload.path("notification").path("icon").asText();
    String clickAction = payload.path("notification").path("click_action").asText();

    // Build the notification string expected by `cmd notification post`
    StringBuilder sb = new StringBuilder();
    sb.append("--title \"").append(title).append("\" ");
    sb.append("--text \"").append(body).append("\" ");
    sb.append("--icon ").append(icon).append(" ");
    sb.append("--category ").append(clickAction).append(" ");

    String cmd = String.format(
        "adb -s %s shell cmd notification post -S %s %s",
        getDeviceId(),
        UUID.randomUUID().toString(),
        sb.toString()
    );

    Process proc = Runtime.getRuntime().exec(cmd);
    proc.waitFor(5, TimeUnit.SECONDS);
    if (proc.exitValue() != 0) {
        throw new RuntimeException("Failed to post notification: " + 
            new String(proc.getErrorStream().readAllBytes()));
    }
}

4. Waiting for the Notification to Appear

Use an explicit wait that polls the notification shade. Appium’s UIAutomator2 driver can query the system view via accessibility id or class name.


public AndroidElement waitForNotification(String title, Duration timeout) {
    WebDriverWait wait = new WebDriverWait(driver, timeout);
    return wait.until(drv -> {
        List<AndroidElement> notifications = drv.findElementsByClassName("android.widget.TextView");
        for (AndroidElement el : notifications) {
            if (el.getText().contains(title)) {
                return el;
            }
        }
        return null;
    });
}

5. Verifying Content and Tapping

Once the notification element is located, expand the notification (if collapsed) and verify the body text. Then perform a tap on the notification to trigger the deep link.


@Test
void welcomePushLaunchesCorrectScreen() throws Exception {
    JsonNode payload = NotificationLoader.load("welcome_push.json");
    sendPushNotification(payload);

    AndroidElement notif = waitForNotification(
        payload.path("notification").path("title").asText(),
        Duration.ofSeconds(10)
    );
    assertNotNull(notif, "Notification not visible within timeout");

    // Expand if needed (some OEMs collapse by default)
    if (!notif.isDisplayed()) {
        notif.click(); // tap to expand
    }

    String bodyText = notif.findElement(By.id("android:text")).getText();
    assertEquals(payload.path("notification").path("body").asText(), bodyText);

    // Tap the notification to launch the app
    notif.click();

    // Verify we landed on the expected screen
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
    AndroidElement welcomeHeader = wait.until(
        d -> d.findElementById("com.example.app:id/welcome_header")
    );
    assertTrue(welcomeHeader.isDisplayed(),
        "Welcome screen not shown after notification tap");
}

6. Handling Silent / Data‑Only Pushes

For silent pushes, there is no UI to inspect. Instead, verify that your app’s FirebaseMessagingService processes the payload and updates local state (e.g., a badge count, a local database entry). You can expose a test‑only ContentProvider or use adb shell cmd package to query a flag.


@Test
void silentPushUpdatesBadgeCount() throws Exception {
    JsonNode payload = NotificationLoader.load("silent_badge.json");
    sendPushNotification(payload);

    // Give the service a moment to run
    Thread.sleep(2000);

    // Query a test-exposed provider for the badge
    Cursor c = driver.executeScript("mobile: shell", ImmutableMap.of(
        "command", "content query --uri content://com.example.app.provider/badge --projection count"
    )).toString();

    assertEquals("1", c.trim(), "Badge count not incremented");
}

7. Making Tests Maintainable

With this skeleton you can expand to cover action buttons, image-rich notifications, grouped notifications, and do‑not‑disturb scenarios by adjusting the wait conditions and the verification steps.

Locator Strategy and Element Identification

Push notifications live in a system‑owned UI layer that varies across OEMs and Android versions. Relying on fragile indexes (e.g., the first TextView in the shade) leads to flaky tests. Instead, adopt a strategy that combines accessibility hints, resource IDs, and dynamic text matching.

Android Notification Shade Locators

Locator TypeWhen to UseExample
accessibility id (content‑description)When the notification sets a content‑description (rare)new MobileBy.AccessibilityId("notification_title")
resource‑idWhen the app’s notification uses a known package‑specific ID (e.g., a custom layout)new By.AndroidUIAutomator('new UiSelector().resourceId("com.example.app:id/notification_title")')
class name + textFallback for stock notifications; match on part of the title/bodynew By.AndroidUIAutomator('new UiSelector().className("android.widget.TextView").textContains("Welcome")')
xpath with index boundsOnly when you can guarantee a small, stable set (e.g., max 3 notifications)//android.widget.TextView[@text='Welcome aboard!']

Best practice: Use a combination of className and textContains wrapped in a custom ExpectedCondition, as shown in the wait helper above. This tolerates minor layout shifts (e.g., extra padding) while still being specific enough to avoid false positives.

iOS Notification Center Locators

XCUITest can access the notification center via springboard elements.


let notification = springboard.otherElements["NotificationCenter"]
    .cells
    .matching(NSPredicate(format: "label CONTAINS[c] %@", title))
    .element

Wait for the element to appear using XCTestCase.expectation(for: NSPredicate, evaluatedWith: notification, handler: nil).

Web Push Notifications (Playwright)

In the browser, notifications appear as OS‑level dialogs that Playwright can interact with after granting permission.


await context.grantPermissions(['notifications']);
await page.goto('https://example.com');
// Trigger a push via service worker or backend
await page.waitForEvent('notification', async notification => {
    expect(notification.title()).toBe('Welcome aboard!');
    expect(notification.body()).toContain('Your journey starts now');
    await notification.click(); // triggers the click action
});

If the notification is shown inside the page (e.g., an in‑app toast), treat it like any other DOM element using standard locators (page.getByText, page.locator).

Dealing with OEM Variations

Create a small NotificationLocatorFactory that returns the appropriate locator based on device.getBuild().getModel() or systemProps. This keeps your test code clean while accommodating fragmentation.

Handling Waits, Synchronization, and Flaky Tests

Flakiness in push‑notification tests usually stems from timing mismatches: the notification may arrive early, late, or be coalesced by the system. Mitigate this with explicit waits, idling resources, and deterministic test data.

Explicit Waits with Custom ExpectedConditions

Instead of Thread.sleep, use WebDriverWait (Appium) or WebDriverWait (Playwright) with a condition that checks both presence and stability of the notification.


public static AndroidElement waitForStableNotification(
    AndroidDriver driver,
    String expectedTitle,
    Duration timeout) {
    new WebDriverWait(driver, timeout)
        .pollingEvery(Duration.ofMillis(250))
        .withTimeout(timeout)
        .ignoring(StaleElementReferenceException.class)
        .until(drv -> {
            List<AndroidElement> els = drv.findElementsByClassName("android.widget.TextView");
            for (AndroidElement e : els) {
                if (e.getText().contains(expectedTitle)) {
                    // Verify the element hasn't moved in the last two polls
                    String prevText = e.getText();
                    try { Thread.sleep(150); } catch (InterruptedException ignored) {}
                    return e.getText().equals(prevText) ? e : null;
                }
            }
            return null;
        });
}

Idling Resources (Espresso) / Async Wait (Playwright)

If your test framework supports idling resources (Espresso) or route waiting (Playwright), register a resource that is busy while the FCM handler is processing the payload.


public class NotificationProcessingIdlingResource implements IdlingResource {
    private volatile boolean idle = true;
    @Override public String getName() { return this.getClass().getName(); }
    @Override public boolean isIdle() { return idle; }
    @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.callback = callback; }

    public void setBusy() { idle = false; }
    public void setIdle() { idle = true; if (callback != null) callback.onTransitionToIdle(); }
}

In your FirebaseMessagingService, call idlingResource.setBusy() at the start of onMessageReceived and setIdle() after you finish updating UI or local state. Espresso will then wait automatically.

Dealing with Notification Coalescing

Android may collapse multiple notifications with the same key into a single entry, showing only the latest. To avoid this, assign a unique notification.tag or notification.id for each test case (e.g., a UUID). In the payload, set:


{
  "notification": {
    "tag": "test_${UUID.randomUUID()}",
    ...
  }
}

When you query the shade, filter by this tag using the extra fields accessible via adb shell dumpsys notification.

Reducing Flake from Device State

Wrap these steps in a @BeforeEach method so each test starts from a known baseline.

Monitoring and Retry Strategies

In CI, configure your test runner to retry flaky tests a limited number of times (e.g., 2 retries) but only after marking the test as “flaky” in a test‑management tool. Use the retry data to identify which notification scenarios need tighter waits or better locators.

Data.

Data Setup, Teardown, and Environment Management

Reliable push‑notification testing requires a repeatable backend that can generate the exact payload you want, without affecting real users. There are three common patterns:

  1. Mock FCM/APNs Server – Run a lightweight stub (e.g., mock-fcm-server npm package) that accepts HTTP POST and forwards the payload to a local broadcast receiver.
  2. Feature Flag / Test Mode – Your production backend checks a special header (X-Test-Mode: true) and, when present, returns a canned payload instead of querying the real user database.
  3. Device‑Side Injection – Use adb shell cmd notification post (Android) or XCUITest XCUIDevice notifications (iOS) to bypass the backend entirely.

Example: Mock FCM Server with Node.js


// mock-fcm.js
const express = require('express');
const app = express();
app.use(express.json());

app.post('/fcm/send', (req, res) => {
    const payload = req.body;
    // In a real test you would forward this to the device via adb or a test-only receiver
    console.log('Mock FCM received:', JSON.stringify(payload, null, 2));
    res.status(200).send({ success: true });
});

app.listen(3000, () => console.log('Mock FCM listening on :3000'));

Start it in your CI pipeline before the test suite:


node mock-fcm.js &
MOCK_PID=$!
# run tests
kill $MOCK_PID

Your Android test then sends an HTTP request to http://10.0.2.2:3000/fcm/send (the emulator’s alias for host localhost) with the JSON payload.

Test Data Versioning

Store notification JSON files under version control. Use a naming convention that reflects the intent: welcome_push_v1.json, welcome_push_v2_image.json. When you update the payload schema, bump the version and update the test’s expected assertions. This makes it trivial to see which notification variants are covered.

Teardown Practices

After each test:

These steps guarantee that a failing test does not leave stray notifications that could cause false positives in the next run.

Running Tests in CI/CD and Reporting

Integrating push‑notification tests into your CI pipeline gives you confidence on every commit. The key is to provision devices or emulators, start the Appium server, launch any mock services, execute the tests, and publish results in a format your team can consume (JUnit XML, HTML report, or test‑management system hook).

CI Example: GitHub Actions with Android Emulator


name: Push Notification Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test-android:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          distribution: temurin
          java-version: '11'

      - name: Install Android SDK
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          target: google_apis
          arch: x86_64
          force-avd-creation: true
          emulator-options: -no-window -no-audio -no-boot-anim

      - name: Start Appium server
        run: |
          npm install -g appium
          appium &> appium.log &
          echo $! > appium.pid

      - name: Start mock FCM
        run: |
          npm ci
          node mock-fcm.js &
          echo $! > mock.pid

      - name: Run tests
        run: |
          mvn test -Dtest=PushNotificationTestSuite

      - name: Upload test results
        uses: actions/upload-artifact@v3
        with:
          name: junit-reports
          path: **/target/surefire-reports/*.xml

      - name: Cleanup
        if: always()
        run: |
          kill $(cat appium.pid) || true
          kill $(cat mock.pid) || true

Adjust the mvn test command to point to your test suite. The android-emulator-runner action handles waiting for the device to boot and unlocking the screen.

Reporting

Parallel Execution

If you have a device farm (Firebase Test Lab, AWS Device Farm, or Sauce Labs), you can shard your notification JSON files across multiple devices. Use a matrix strategy:


strategy:
  matrix:
    device: [pixel_4_api_33, pixel_5_api_33, pixel_6_api_33]

Each job receives its device identifier, installs the app under test, and runs the same test suite. This catches OEM‑specific quirks (e.g., MIUI’s grouped notifications) without multiplying test code.

Flake Detection

Enable your CI to automatically label a test as “flaky” if it passes in one retry and fails in another. Many CI platforms (GitHub Actions, GitLab CI) have built-in retry controls; combine with a test‑metadata file that records the attempt count. Periodically review the flaky list and tighten waits or improve locators.

Autonomous Exploration Bootstrapping Push Notifications Testing (SUSA Section)

Writing the first set of push‑notification tests can be time‑consuming, especially when you are unsure which notification variants your app actually produces in the wild. An autonomous exploration platform like SUSA can observe real user interactions, discover notification triggers, and generate starter test scripts without you writing a single line of code.

How SUSA Helps

  1. App Exploration – Upload your APK or point SUSA at a staging URL. The agent explores the app using a variety of personas (curious, impatient, novice, etc.), automatically granting notification permissions and interacting with UI elements that could cause a push (e.g., “Enable reminders”, “Subscribe to news”, “Complete purchase”).
  2. Event Capture – Whenever the app receives a push notification (FCM/APNs), SUSA logs the payload, the timestamp, the UI state before and after, and any deep‑link action associated with the notification.
  3. Script Generation – From the collected events, SUSA emits a baseline Appium (Android) or Playwright (web) test suite that:
  1. Cross‑Session Learning – Subsequent runs remember which notification templates were already covered, focusing the explorer on uncovered paths (e.g., a promotion that only appears after three consecutive days of inactivity).
  2. Regression Safe‑Guard – The generated tests are added to your CI pipeline as a safety net; any change that breaks a previously observed notification flow will cause a failure.

Example Output (Appium Java)

SUSA might produce a test class like:


public class GeneratedPushNotificationTest {
    @Test
    public void testWelcomePushFromOnboarding() throws Exception {
        // Payload captured during exploration
        JsonNode payload = NotificationLoader.load("explored/welcome_onboard.json");
        NotificationInjector.send(payload); // uses adb shell cmd notification post
        AndroidElement notif = waitForNotification(
            payload.path("notification").path("title").asText(),
            Duration.ofSeconds(10)
        );
        assertNotNull(notif);
        assertEquals(payload.path("notification").path("body").asText(),
                     notif.findElement(By.id("android:text")).getText());
        // If the notification had a click_action, SUSA adds the tap:
        if (payload.path("notification").has("click_action")) {
            notif.click();
            // verify deep‑link destination
            new WebDriverWait(driver, Duration.ofSeconds(15))
                .until(d -> d.findElementById("com.example.app:id/welcome_screen"));
        }
    }
}

You can then refine the generated tests—parameterize the JSON, improve waits, add assertions for image assets—but the bulk of the boilerplate is already there. This approach dramatically reduces the initial investment and ensures your automated suite reflects real‑world usage patterns rather than hypothetical scenarios.

> Note: SUSA is mentioned here strictly to illustrate how autonomous exploration can bootstrap notification testing. The concepts and code shown apply equally if you build your own exploration harness or rely on manual test case authoring.

Checklist for Reliable Push‑Notification Testing

✅ ItemWhy It MattersHow to Verify
Unique notification tag or IDPrevents coalescing, makes each test distinctInspect adb shell dumpsys notification for the tag
Explicit wait for notification appearanceEliminates Thread.sleep flakinessTest passes consistently on slow emulator and fast device
Payload validation (title, body, image, data)Guarantees the backend sends what the UI expectsAssert each field matches the JSON source
Action verification (tap → deep link or silent‑push side‑effect)Confirms the notification is functional, not just presentAfter tap, assert expected screen or DB change
Permission handling (grant/revoke)Some devices require runtime permission; missing it yields silent failuresRun a test that revokes permission and expects no notification
Do‑Not‑Disturb / battery‑optimization offSystem may suppress or delay notificationsCheck adb shell cmd notification getInterruptionFilter returns 2
Test data versioned in repoEnables traceability and schema evolution checks

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free