Testing Android applications

How to test Android notifications and deep links

A notification can contain the right text and still take the user to the wrong screen. Alternatively, the backend may create an event but Android displays nothing because the user has not allowed notifications. A reliable test therefore checks more than a single card in the system tray: it follows the entire flow from the server event to the specific content opened in the application.

One scenario crosses several layers

At least four parts of the system work together for an “Order dispatched” message:

  1. The backend creates an event. The order changes state and the server prepares a message for a particular app instance or user.
  2. A delivery service accepts the message. This may be Firebase Cloud Messaging (FCM), for example. Acceptance of the request by the service does not yet prove that a notification appeared on the device.
  3. Android and the app process the message. The outcome depends on the message type, app state, system version, notification permission, and channel configuration.
  4. A tap opens the destination. A deep link, which points to a specific place in the app, must open the correct order and create a sensible navigation history.

If a test begins by tapping a prepared notification, it does not verify the backend or delivery. If it ends by checking text in the system tray, it does not verify the deep link. These partial tests are valuable for diagnosis, but at least one critical scenario should cover the complete agreed flow.

Build the matrix around state, not the number of phones

The most important combinations arise from the app and account states:

Add allowed, denied, and not-yet-requested notification states to the matrix. Starting with Android 13, API level 33, ordinary notifications use the POST_NOTIFICATIONS runtime permission. The official Android documentation also provides ADB commands for preparing individual permission states, so a test does not have to depend on what a previous run left on the device.

A realistic example: an order changes state

Consider test order A-4821, which is waiting to be dispatched. The test signs in with a dedicated account, opens the app at least once so that it registers to receive messages, and associates the device identifier with that account. It then changes the order status to “dispatched” through a safe test API. The backend should create one event containing the order identifier, user, and deep-link destination.

The server record is checked first: the event was created only once, belongs to the correct account, and does not point to another order. The app is then moved to the background and the test waits for the notification. The words “Order dispatched” alone do not establish success. After the tap, order A-4821, its dispatched status, and the corresponding items must be visible. The Back action should lead to the order list rather than open the same detail again.

It is useful to divide the same business case into smaller variants. For a cold start, the test stops the app process before the tap but preserves its data and signed-in state. For a warm state, it leaves the app on another order and verifies that the new deep link replaces the content. When signed out, the app should redirect to sign-in and continue to A-4821 after successful authentication; if the product deliberately follows another rule, the test should confirm the agreed safe destination instead.

Finally, run a negative variant with notification permission denied. The backend event may still be correct, but a system notification should not be expected. The test then checks whether opening the app shows the new order status and whether the product gives the user an appropriate explanation of how notifications can be enabled. This variant distinguishes a product defect from the user’s valid choice.

A method from the event to the destination screen

1. Prepare unambiguous test data

Create an order with a unique identifier and a known initial state, for example. Use the same identifier in backend logs, the message, and the test expectation. If something fails, you can then connect the server event with what the device received and opened.

2. Verify event creation separately

An API or integration-layer test confirms that the correct business event created a message for the correct recipient and included the required destination. A separate test at this layer helps distinguish a backend defect from a device problem. There is no need to use production distribution when a safe test endpoint or isolated test environment is available.

3. Set the app and permission state

Before the run, explicitly decide whether the app should be running, backgrounded, or stopped. Set the signed-in state and notification permission as well. Appium and Maestro can control both the app and system elements; Maestro also supports setting selected permissions on launch. The exact capability still needs to be checked for the driver, Android version, and device type in use.

4. Distinguish delivery from display

Check that the message reached the intended device, the correct title, body, and channel appeared, and no duplicate was created. When permission is denied, understandable app behaviour may be the correct result rather than a visible system notification. The test should not silently enable permission and thereby bypass the real user journey.

5. Tap and verify meaning, not just a screen title

After the tap, verify the identifier or content of the specific order, message, or task. A heading that merely says “Detail” is not enough. Check the Back action too: it should take the user to the expected place rather than to an empty screen or out of the app.

Test the deep link directly as well, without a notification. Android supports launching a URI with ADB, while Appium (mobile: deepLink) and Maestro (openLink) offer corresponding operations. This separates a routing defect from a delivery defect. For links on a domain you control, verify Android App Links, including the manifest and assetlinks.json; depending on the system version and user settings, an ordinary web link may open in the browser instead.

6. Collect evidence from every layer

Put a unique correlation identifier in the backend event, the test dispatch record, and, where possible, the message’s data payload. It does not have to be shown to the user. If the scenario fails, the identifier connects the server log, delivery-service response, device log, and specific test run without relying on approximate timestamps.

Define evidence of success for each layer in advance. The backend may confirm creation of one message, the delivery service may confirm acceptance of the request, the device may show the correct channel, and the application may show specific content after the tap. These signals are not interchangeable: service acceptance does not prove display, while an open detail does not prove that the user arrived through a system notification.

Do not verify an exact delivery time with one short fixed wait. Network conditions, power-saving modes, and system scheduling can affect timing. Set a reasonable time window for the purpose of the test, record the actual time of each stage, and retain diagnostics if the window is exceeded. If the product has time-critical alerts, define the required limit as a separate operational requirement and measure it in a suitable environment rather than deriving it from one automated run.

Edge states that reveal the most problems

Check a missing or deleted destination, an expired session, sign-out performed on another device, and two notifications opened in reverse order. In these cases, the app should show a safe, understandable fallback state rather than another user’s content or an endless loading indicator.

System dialogs and delivery behaviour may vary across Android versions and manufacturer customisations. Fast scenarios can therefore be developed on an emulator, while the decisive flow should also be part of targeted coverage on physical devices. We discuss device selection in Android testing: emulator vs physical device.

What to run frequently and what to keep for broader runs

A direct test of the deep link, navigation, and signed-in or signed-out state can run quickly after every relevant change. It does not wait for external delivery and identifies routing defects more precisely. A backend integration test can likewise verify construction of the correct payload without interacting with Android system UI.

The complete path through a real delivery service and the system tray is slower and more sensitive to the environment. For the most important notification types, place it in a scheduled run or pre-release check and use a limited selection of Android versions and devices based on risk. When automating the system tray, prefer available system identifiers and element meaning over coordinates; the exact label of a system button may vary with the system language or version.

This separation does not create competing tests. The fast layers protect application rules and navigation, while a smaller number of complete runs verify the real connection between layers. If the complete scenario fails, the partial tests help locate the problem.

What you gain

Dividing the flow into layers shortens diagnosis. When the test fails, the team can see whether the event was never created, the message was not delivered, the system did not display it, or the app handled the link incorrectly. The state matrix also covers differences that one successful tap while the app is already open will not reveal.

Next step

Choose one high-impact notification and map its path from the backend event to the destination screen. Prepare a separate event test, a separate deep-link test, and one complete signed-in cold-start scenario. Then add further combinations based on risk rather than mechanically covering every screen.

Related topics

You might also be interested in

We will verify the decisive scenarios on real devices

Automated testing of native and hybrid Android applications, with critical runs on physical or cloud devices.