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
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 Aspect | Manual Feasibility | Automation ROI | Recommended Approach |
|---|---|---|---|
| Delivery timing (latency < 2 s) | Low (requires precise stopwatch) | High | Automated with timestamp logging |
| Content correctness (title, body, image) | Medium (visual check) | Medium | Automated via UI inspection or API validation |
| Action button tap (deep link) | Medium (requires device interaction) | High | Automated UI tap + navigation assert |
| Silent / background push (no UI) | Very low (no visible cue) | High | Automated via backend receipt verification or silent‑push handling code |
| Permission prompts (allow/deny) | Medium (dialog handling) | Medium | Automated if dialog appears predictably |
| Do‑Not‑Disturb / battery‑optimization impact | Low (device‑specific settings) | Low | Mostly manual or device‑farm matrix |
| Localization of notification text | Medium (language switch) | Medium | Automated with locale‑specific resource checks |
| High‑volume burst (10+ notifications) | Very low (tedious) | High | Automated 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
- Regression safety – Every release that touches the push‑notification pipeline (FCM/APNs config, payload builder, foreground service) should run the automated suite.
- Cross‑device confidence – Running the same script on a fleet of emulators or real devices surfaces timing differences that manual testers miss.
- Performance baselines – By logging timestamps from receipt to UI display you can detect regressions in latency introduced by SDK updates.
- Release gating – Integrate the suite into your pull‑request workflow; a failing notification test blocks merge until the root cause is fixed.
- 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
| Framework | Platform | Language | Push Notification Support | Setup Complexity | Community Maturity | Typical Use Case |
|---|---|---|---|---|---|---|
| Appium (Android) | Android | Java, JS, Python, Ruby | Can intercept system notifications via UIAutomator2; can read notification shade | Medium (requires Android SDK, emulator/device) | High | Native Android apps |
| XCUITest | iOS | Swift/Obj‑C | Access to UNUserNotificationCenter; can simulate taps on banners | Low (bundled with Xcode) | High | Native iOS apps |
| Playwright | Web (Chromium/Firefox/WebKit) | JS/TS, Python, Java, .NET | Can grant notification permission, listen to notification events, verify payload | Low (single binary) | Growing | PWAs, SPA, web push |
| Firebase Test Lab + Robo Script | Android/iOS | Java/Kotlin/Swift | Can trigger FCM via test‑lab hooks; limited UI verification | Medium (requires Firebase project) | High | Large device farm testing |
| Espresso | Android | Java/Kotlin | No direct notification shade access; relies on IdlingResource + BroadcastReceiver | Low (if already using Espresso) | High | Unit‑style UI tests within Android test source set |
| Detox | React Native | JS | Can simulate push via react-native-push-notification mock; works with Jest | Medium | Medium | RN 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
- Is your app purely native Android? → Start with Appium + UIAutomator2.
- Do you also need iOS coverage? → Add XCUITest in parallel; keep test data JSON shared.
- Is a significant portion of your audience using a PWA or web push? → Add Playwright tests that run against the same staging URL.
- 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
- Parameterize the notification payload filename via
@ValueSource(JUnit 5) or@Parameters(TestNG) so a single test method runs against many JSON files. - Encapsulate platform‑specific commands (
adb shell cmd notification post) inside aNotificationInjectorinterface with Android and iOS implementations. - Use Page Objects for the notification shade and for the deep‑link target screens; this isolates locator changes.
- Log the raw payload and the timestamps of send/receive/tap to a test‑log file; this aids flake analysis.
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 Type | When to Use | Example |
|---|---|---|
accessibility id (content‑description) | When the notification sets a content‑description (rare) | new MobileBy.AccessibilityId("notification_title") |
resource‑id | When 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 + text | Fallback for stock notifications; match on part of the title/body | new By.AndroidUIAutomator('new UiSelector().className("android.widget.TextView").textContains("Welcome")') |
xpath with index bounds | Only 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
- Xiaomi/MIUI: Notifications may be grouped under a “card”. Use the card’s
resource-id(com.android.systemui:id/notification_card) as a parent, then locate children. - Samsung OneUI: The shade uses
android.widget.FrameLayoutcontainers withcontent-desccontaining the app label. Match on that then dive into innerTextViews. - Huawei EMUI: Some notifications are rendered as heads‑up banners; they appear briefly at the top. Use a short timeout and locate by
resource-idof the heads‑up layout (com.huawei.android.internal.policy.impl.PhoneWindowManager$HeadsUpManagerView).
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
- Clear the notification shade before each test:
adb shell service call notification 1(requires root) or simply swipe away all notifications via UIAutomator2 (driver.findElements(By.id("android:id/action_button"))). - Ensure Do‑Not‑Disturb is off:
adb shell cmd notification setInterruptionFilter 2. - Disable battery optimization for the test app:
adb shell cmd appops set.IGNORE_BATTERY_OPTIMIZATION allow
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:
- Mock FCM/APNs Server – Run a lightweight stub (e.g.,
mock-fcm-servernpm package) that accepts HTTP POST and forwards the payload to a local broadcast receiver. - 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. - Device‑Side Injection – Use
adb shell cmd notification post(Android) orXCUITestXCUIDevicenotifications (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:
- Clear app data (
adb shell pm clear) to remove any local state changes from notification handling. - Remove test‑only notifications:
adb shell cmd notification cancelall(requires API 24+; otherwise loop through IDs). - Reset mock server state: If your mock stores received calls, clear its internal list.
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
- JUnit XML – Most CI systems ingest this natively; configure your test runner (TestNG, JUnit5, Jest) to produce it.
- Allure – Provides a rich HTML timeline with screenshots attached at each step. Add the Allure Maven/Gradle plugin and call
Allure.addAttachmentafter each notification verification. - Test Rail / Zephyr – Push results via their REST API using the
run_idandcase_idmapping you maintain in a spreadsheet.
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
- 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”).
- 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.
- Script Generation – From the collected events, SUSA emits a baseline Appium (Android) or Playwright (web) test suite that:
- Launches the app,
- Sends the captured payload via
adb shell cmd notification post(or mock FCM), - Waits for the notification using the same locator strategy it observed,
- Verifies content and taps the notification if an action exists.
- 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).
- 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
| ✅ Item | Why It Matters | How to Verify |
|---|---|---|
| Unique notification tag or ID | Prevents coalescing, makes each test distinct | Inspect adb shell dumpsys notification for the tag |
| Explicit wait for notification appearance | Eliminates Thread.sleep flakiness | Test passes consistently on slow emulator and fast device |
| Payload validation (title, body, image, data) | Guarantees the backend sends what the UI expects | Assert each field matches the JSON source |
| Action verification (tap → deep link or silent‑push side‑effect) | Confirms the notification is functional, not just present | After tap, assert expected screen or DB change |
| Permission handling (grant/revoke) | Some devices require runtime permission; missing it yields silent failures | Run a test that revokes permission and expects no notification |
| Do‑Not‑Disturb / battery‑optimization off | System may suppress or delay notifications | Check adb shell cmd notification getInterruptionFilter returns 2 |
| Test data versioned in repo | Enables 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