How to Test Deep Links on Android (Complete Guide)

Deep links are the entry points that let other apps, web pages, or notifications launch a specific screen inside your Android application. When they work, users move seamlessly from a marketing email

June 28, 2026 · 15 min read · How-To Guides

Why Deep Link Testing Matters on Android

Deep links are the entry points that let other apps, web pages, or notifications launch a specific screen inside your Android application. When they work, users move seamlessly from a marketing email to a product detail page, or from a chat message to a conversation thread. When they fail, the user sees a blank screen, an error toast, or worse—a crash that leads to a one‑star review.

In production, deep‑link bugs often surface only after a release because:

Because deep links sit at the intersection of the Android framework, your UI layer, and external triggers, they deserve a dedicated test strategy that covers happy paths, error paths, accessibility, and security concerns.

---

Android Deep Link Fundamentals

Before writing tests, understand how the system resolves a deep link.

Intent Filters

An activity declares which URIs it can handle via an in AndroidManifest.xml. A typical filter looks like:


<activity android:name=".ProductDetailActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="shop.example.com"
            android:pathPrefix="/product" />
    </intent-filter>
</activity>

When the system receives an Intent with ACTION_VIEW and a data URI that matches the filter, it launches the activity. If multiple apps match, the resolver shows a disambiguation dialog unless android:autoVerify="true" and the associated domain passes the Digital Asset Links verification.

Handling the Intent

Inside the activity you can retrieve the URI in two callbacks:

A robust implementation extracts needed parameters, validates them, and then navigates to the appropriate UI state. Missing validation is a common source of crashes.

Testing Tools Provided by the Platform

Understanding these basics lets you build a test matrix that exercises every decision point.

---

Comprehensive Test Matrix

Below is a table that organizes deep‑link test cases by category, sub‑case, expected outcome, and notes on automation feasibility.

CategorySub‑caseDescriptionExpected ResultAutomation Notes
Happy PathHP1Launch from a browser with a valid HTTPS URL matching the filter.Activity opens, UI shows correct data, no crash.adb shell am start -a android.intent.action.VIEW -d "https://shop.example.com/product/123"
HP2Launch from another app using a custom scheme (myapp://).Same as HP1.adb shell am start -a android.intent.action.VIEW -d "myapp://product/123"
HP3Launch when activity already in stack (singleTop) receives a new deep link.onNewIntent called, UI updates accordingly.Use adb shell am start ... twice; verify state change.
Error PathsEP1URL with correct host but invalid path (e.g., /product/abc when expecting numeric ID).Activity opens but shows an error toast or empty state; no crash.Same adb command; assert error UI.
EP2Missing required query parameter (e.g., no token when required).Graceful handling – show login prompt or error.Add -d "https://shop.example.com/product/123" without token.
EP3URL with unsupported scheme (e.g., ftp://).System shows “No app can open this URL” or opens default handler.Verify no activity launch; check logcat for ActivityNotFoundException.
EP4Deep link that triggers a permission‑protected component without granting permission.System shows permission dialog; if denied, activity does not start.Use adb shell pm grant / revoke before launch.
Edge CasesEC1Launch from a cold start vs. warm start (app killed vs. background).UI state consistent; any async loading handled.Use adb shell am force-stop then launch; compare with adb shell am start after adb shell am start -n /. to background.
EC2Deep link containing special characters requiring URL encoding (%20, %3F).Decoded correctly; no crash.Include encoded chars in adb command.
EC3Very long URI (>2000 characters) – tests buffer limits.Activity opens; system truncates or throws UriTooLongException (handled gracefully).Generate long string with python -c "print('a'*2500)".
EC4Launch when device is locked (keyguard showing).Activity appears after unlock, or system shows notification to open app.Use adb shell input keyevent KEYCODE_WAKEUP before/after.
EC5Deep link invoked via NFC or Bluetooth share.Same UI as manual launch; intent extras preserved.Simulate with adb shell am start -a android.nfc.action.NDEF_DISCOVERED -d "myapp://..."
AccessibilityAC1Talk to NFC emulator.
AccessibilityAX1TalkBack enabled; deep‑to the content description matches expected.Use uiautomator to verify spoken feedback.
AC2Dynamic font size changes (large text).Layout does not clip essential info.Change font scale via Settings → Accessibility → Font size, then launch deep link.
AC3Color contrast verification on the launched screen (WCAG AA).Text and icons meet contrast ratio.Run Android Accessibility Test Framework or use adb shell am broadcast -a com.google.android.accessibilityfeedback.perform_check.
Security & PrivacySE1Deep link that attempts to navigate to a protected admin screen without proper auth token.Redirects to login or shows error; no data leakage.Include token‑less URL; assert login screen appears.
SE2Deep link containing JavaScript injection attempt (javascript:alert(1)).System treats it as data; no script execution.Verify no toast or dialog from script.
SE3Link that tries to exploit intent redirection (e.g., myapp://evil.com/path).Host validation fails; activity not launched or shows error.Test with malicious host; ensure filter rejects.
SE4Deep link that carries personally identifiable information (PII) in query params; ensure it is not logged.No PII appears in logcat or crash reports.Launch with user_id=12345 and inspect logs with adb logcat.
RegressionRG1After a UI refactor, previously working deep link still opens correct screen.No change in behavior.Keep a baseline set of adb commands; run on each CI build.
RG2After updating targetSdkVersion, implicit intent behavior unchanged.Same as before.Verify with adb shell pm get-max-users etc.

*Feel free to extend the table with product‑specific cases (e.g., deep links that trigger in‑app purchases or open a webview).*

---

Manual Testing Approach

A manual test plan gives you immediate feedback and helps you spot subtle UI glitches that automated checks might miss. Follow these steps for each deep‑link variant in the matrix.

1. Prepare the Device

2. Launch the Deep Link via ADB

The basic command:


adb shell am start \
    -a android.intent.action.VIEW \
    -d "https://shop.example.com/product/123?token=abc" \
    -n com.example.app/.MainActivity   # explicit component optional

*Add -f 0x10000000 (FLAG_ACTIVITY_CLEAR_TOP) if you want to simulate a fresh task.*

3. Observe the Result

4. Validate State

If the deep link carries parameters, verify that the UI reflects them. For example, a product ID should display the product’s name and price. You can inspect view hierarchies with adb shell uiautomator dump /tmp/ui.xml and then grep for expected text.

5. Test Edge Conditions

6. Document Findings

Create a simple spreadsheet with columns: Test ID, Command, Expected, Actual, Pass/Fail, Notes. Attach a screenshot or log snippet for failures.

Manual testing is valuable for exploratory work, but it does not scale across dozens of devices or frequent releases. The next section shows how to automate the same checks.

---

Automated Testing Strategies

Automation turns the manual checklist into repeatable CI jobs. Below are the most effective layers for Android deep‑link validation.

Unit‑Level Intent Validation

Although deep links are system‑level concerns, you can unit‑test the intent‑parsing logic. Extract a plain‑Java/Kotlin function that receives an Intent and returns a parsed model (e.g., DeepLinkData).


// DeepLinkParser.kt
fun parse(intent: Intent?): DeepLinkData? {
    val data = intent?.data
    return if (data == null) null else {
        val productId = data.getQueryParameter("productId")
        val token = data.getQueryParameter("token")
        DeepLinkData(productId, token)
    }
}

Write JUnit tests that feed various URIs (valid, malformed, missing params) and assert the returned model or null. This catches logic errors before the UI is involved.

Instrumented Tests with Espresso

Espresso runs on a device or emulator and can launch an intent directly via IntentTestRule.


@RunWith(AndroidJUnit4::class)
class DeepLinkEspressoTest {

    @get:Rule
    val intentRule = IntentTestRule(MainActivity::class.java, true, false)

    @Test
    fun validProductLink_opensDetailScreen() {
        val uri = Uri.parse("https://shop.example.com/product/42?token=xyz")
        val intent = Intent(Intent.ACTION_VIEW, uri)
        intentRule.launchIntent(intent)

        // Verify UI elements
        onView(withId(R.id.product_name)).check(matches(isDisplayed()))
        onView(withText("Product 42")).check(matches(isDisplayed()))
    }
}

*Advantages*: runs fast, integrates with Gradle, gives you IDE debugging.

*Limitations*: cannot interact with the system chooser dialog; you must specify the component explicitly (IntentTestRule does that).

UI Automator for Chooser Handling

When you want to test the scenario where multiple apps can handle the URI (e.g., a web URL that could open in Chrome or your app), you need UiAutomator to click the “Open with” dialog.


@RunWith(AndroidJUnit4::class)
public class DeepLinkChooserTest {

    @Test
    public void webUrl_ShowsChooserAndSelectsApp() throws Exception {
        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        Intent intent = new Intent(Intent.ACTION_VIEW,
                Uri.parse("https://shop.example.com/product/99"));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);

        // Wait for chooser dialog
        UiObject chooser = new UiObject(new UiSelector()
                .className("android.widget.FrameLayout")
                .descriptionContains("Complete action using"));
        assertTrue(chooser.waitForExists(5000));

        // Click your app’s entry
        UiObject appEntry = new UiObject(new UiSelector()
                .textContains("MyApp"));
        appEntry.click();

        // Verify your activity launched
        UiObject productName = new UiObject(new UiSelector()
                .resourceId("com.example.app:id/product_name"));
        assertTrue(productName.waitForExists(5000));
    }
}

*Tip*: Add adb shell settings put global window_animation_scale 0 and similar to speed up animations during UI Automator runs.

Using adb in Scripts

For CI pipelines that prefer shell scripts, wrap the adb launch and verification steps in Bash or Python. Example Bash snippet:


#!/usr/bin/env bash
PACKAGE="com.example.app"
ACTIVITY=".MainActivity"
URL="https://shop.example.com/product/123?token=abc"

adb shell am start -a android.intent.action.VIEW -d "$URL" -n $PACKAGE/$ACTIVITY
sleep 2   # give time for UI to render

# Simple UI dump check
adb shell uiautomator dump /tmp/ui.xml
if grep -q "Product 123" /tmp/ui.xml; then
    echo "PASS"
else
    echo "FAIL"
    adb shell uiautomator dump /tmp/fail_$(date +%s).xml
fi

You can extend this to loop over a CSV of test cases, collect results, and publish a JUnit XML report.

Firebase Test Lab Integration

If you need to verify across a matrix of OS versions and device models, upload your APK (or App Bundle) to Firebase Test Lab and run an instrumentation test that includes the Espresso or UiAutomator deep‑link tests.


    --type instrumentation \
    --app app-debug.apk \
    --test deep-link-androidTest Lab will automatically shard tests across devices and give you a consolidated result page.  

### Cross‑Session Learning with Autonomous Exploration  

Traditional scripts only follow the paths you encode. An autonomous QA agent, however, can discover deep links that are not documented or that appear only under certain user personas.  

* **How it works** – The agent installs the app, then explores the UI using a combination of random seeds, heuristics, and learned models. Each time it encounters an intent that can be launched (e.g., a “Share” button that builds a URI), it records the URI and later attempts to launch it via adb.  
* **Personas** – By configuring profiles such as “elderly” (long press duration, slower scroll) or “adversarial” (rapid taps on input), the agent can surface deep‑link bugs that only manifest edits, malformed URI injection), the agent stresses edge cases that manual testers might overlook.  
* **Learning** – The agent remembers which URIs lead to crashes, ANRs, or dead ends. On subsequent runs it prioritizes novel URIs while avoiding known dead paths, increasing coverage over time.  

When you integrate a tool like **SUSA** into your CI, you get an extra layer of validation: after the scripted suite runs, the autonomous agent explores the built APK for a configurable number of minutes, logs any deep‑link failures, and generates regression scripts (Appium for Android, Playwright for web) that you can add to your test suite. This approach catches regressions introduced by refactors that inadvertently break a deep link only reachable through a specific UI flow (e.g., a hidden “Promo code” screen).  

---

## Edge Cases That Appear Only in Production  

Even with a solid matrix, certain defects surface only after the app reaches real users. Below are the most common production‑only deep‑link pitfalls and how to mitigate them.  

### 1. Link Hijacking via Malicious Intent Filters  

If another app declares an identical or broader `<intent-filter>`, the system may resolve to the wrong handler. This is especially risky when you use a custom scheme (`myapp://`) without verification.  

*Mitigation*:  
* Use HTTPS URLs with `android:autoVerify="true"` and host a Digital Asset Links file at `https://<host>/.well-known/assetlinks.json`.  
* In your CI, run `adb shell pm get-links <package>` to verify the system has granted auto‑verify status.  

### 2. PendingIntent Mutability  

When you create a `PendingIntent` for a notification that launches a deep link, using the mutable flag (`PendingIntent.FLAG_MUTABLE`) on Android 12+ is required. Forgetting this causes a silent drop of the notification tap.  

*Mitigation*:  
* Lint rule: enforce `PendingIntent.FLAG_IMMUTABLE` only when you truly don’t need mutability.  
* Add a unit test that builds the notification and asserts the flag set.  

### 3. Deep Link Initiated from Background Service  

A background job (e.g., WorkManager) may construct a deep link to inform the user about a completed upload. If the service attempts to start an activity without setting `FLAG_ACTIVITY_NEW_TASK`, the launch fails with `android.util.AndroidRuntimeException: Unable to start activity`.  

*Mitigation*:  
* Always add `FLAG_ACTIVITY_NEW_TASK` when starting an activity from a non‑activity context.  
* Write a test that triggers the WorkManager and verifies the activity appears via UiAutomator.  

### 4. Data Loss During Process Kill  

Android may kill your process while a deep link is pending (e.g., the user taps a notification while the system is low on memory). If your activity relies on data stored in a singleton or static field, that data will be null after recreation.  

*Mitigation*:  
* Persist required parameters to the `Intent` extras or to a `SavedStateHandle` (ViewModel).  
* In your Espresso test, simulate a low‑memory kill with `adb shell am kill <package>` right after launching the deep link, then verify the UI still shows correct data.  

### 5. Time‑Sensitive Tokens  

Some deep links embed short‑lived tokens (e.g., for password reset). In production, a user may click the link minutes after it was generated, causing expiration.  

*Mitigation*:  
* Include a timestamp in the token and validate it server‑side.  
* In tests, generate a token with a known expiration and verify the UI shows an appropriate error when the token is stale.  

### 6. Locale‑Specific Parsing  

If your deep link contains numbers formatted according to the user locale (e.g., `price=1,234.56`), a naïve `Float.parseFloat` will crash on devices where the decimal separator is a comma.  

*Mitigation*:  
* Always use `NumberFormat.getInstance(Locale.US)` or server‑side canonical representation.  
* Add a test matrix that iterates over a set of locales (`Locale.getAvailableLocales()`) and launches the deep link with locale‑specific formatting.  

### 7. NFC/Beam Initiated Links  

When a deep link arrives via Android Beam or NFC, the system may deliver the intent with extra flags like `FLAG_ACTIVITY_BROUGHT_TO_FRONT`. If your manifest uses `launchMode="singleTask"` but you forget to handle `onNewIntent`, the UI may stay on an outdated screen.  

*Mitigation*:  
* Override `onNewIntent` and call `setIntent(intent)` before extracting data.  
* Write a UiAutomator test that uses `adb shell am start -a android.nfc.action.NDEF_DISCOVERED -d "myapp://..."` and asserts the UI updates.  

By adding these production‑focused checks to your test matrix (see the EP, EC, and SE rows above), you reduce the chance that a deep‑link bug slips through to users.  

---

## Checklist for Release Readiness  

Before you promote a build to production, run through this concise list. Each item can be automated or verified manually; tick the box when satisfied.  

| ✅ Item | How to Verify |
|--------|----------------|
| **Manifest correctness** – all `<intent-filter>` entries have proper `scheme`, `host`, and `path*` attributes. | `grep -A5 -B1 "<intent-filter>" app/src/main/AndroidManifest.xml` and review. |
| **Auto‑verify status** – for HTTPS links, the system has granted verification. | `adb shell pm get-links <package>`; look for `domains: shop.example.com` with `status: granted`. |
| **Intent parsing unit tests** – 100 % coverage of `parse()` function. | Run `./gradlew testDebugUnitTest --tests *DeepLinkParserTest*`. |
| **Espresso happy‑path tests** – at least one test per major deep‑link route. | `./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.package=com.example.app.deeplink`. |
| **UiAutomator chooser test** – verifies system dialog handling when multiple apps match. | Run the chooser test on an emulator with a second app installed that also claims the URI. |
| **Accessibility validation** – TalkBack reads the launched screen correctly. | Enable TalkBack, launch a deep link via adb, listen for spoken feedback; or run `adb shell am broadcast -a com.google.android.accessibilityfeedback.perform_check`. |
| **Security sanity** – no PII in logcat, no privilege escalation via malformed URI. | `adb logcat -d | grep -i "token\|email"` after launching a deep link with fake PII; ensure nothing appears. |
| **Cold start vs warm start consistency** – UI state identical after force‑stop vs background launch. | Launch deep link, force‑stop, relaunch; compare screenshots with `adb exec-out screencap -p`. |
| **Locale robustness** – deep link works under at least three different locales (en_US, fr_FR, ja_JP). | Change locale via `adb shell setprop persist.sys.language <lang> && adb shell reboot`, then launch. |
| **Version‑target safety** – same behavior on minSdkVersion and targetSdkVersion devices. | Run the test matrix on an API 21 emulator and an API 34 emulator (or Firebase Test Lab). |
| **Autonomous agent pass** – optional but recommended: run SUSA exploration for 5 min and confirm zero new deep‑link failures. | `susatest-agent explore --apk app-debug.apk --minutes 5 --output report.json`. |

If any item fails, treat it as a blocker and fix before proceeding to the next release candidate.  

---

## Closing Takeaways  

Deep links are a powerful conduit for user acquisition, engagement, and cross‑app integration, but they are also a frequent source of crashes, ANRs, and security gaps when not tested thoroughly.  

* Start with a solid foundation: verify your manifest intent filters, unit‑test the parsing logic, and automate the happy paths with Espresso.  
* Broaden coverage with UiAutomator to handle system choosers, background launches, and accessibility checks.  
* Use adb‑based scripts or Firebase Test Lab to exercise edge cases such as cold starts, locale variations, and long URIs.  
* Add production‑focused tests for link hijacking, PendingIntent mutability, process‑kill data loss, and time‑sensitive tokens.  
* Consider augmenting your scripted suite with an autonomous, persona‑driven explorer (tools like SUSA) that can discover undocumented deep links and surface bugs that static scripts never anticipate.  

By following the matrix, checklist, and automation strategies outlined above, you will ship Android apps where every deep‑link behaves predictably, securely, and accessibly for every user—whether they arrive from a email, a QR code, a notification, or a malicious actor trying to probe your defenses.  

Happy testing!

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