How to Test Deep Links in Mobile Apps (Android + iOS)
Deep links are the hyperlinks of mobile. A URL opens the right screen in your app, preserves context, and handles the case where the app is not installed. They power email campaigns, push notification
Deep links are the hyperlinks of mobile. A URL opens the right screen in your app, preserves context, and handles the case where the app is not installed. They power email campaigns, push notifications, referrals, shared content. When they break, everything downstream breaks — and deep link bugs are often invisible in analytics until someone checks.
What deep links actually are
Three flavors:
- Custom scheme URIs —
myapp://product/123. Work on-device, break when shared anywhere that does not recognize the scheme. - Universal Links (iOS) / App Links (Android) —
https://myapp.com/product/123. Open the app if installed, open the website otherwise. The modern default. - Deferred deep links — work even if the app is not installed. Install, then open to the intended destination. Implemented via Firebase Dynamic Links, Branch, AppsFlyer, or custom install-referrer logic.
What to test
Installed app
- Universal/App Link from any context (email, SMS, browser, other apps) → opens app
- Custom scheme from same contexts → opens app where supported
- App in foreground → navigates without restart
- App in background → brings to foreground and navigates
- App killed → cold starts and navigates (not home screen first)
- Multiple chained deep links (rapid taps) → last one wins, no crash
Not installed
- Universal/App Link → opens your website
- Deferred deep link (if used) → installs app → post-install opens to the destination
- Post-install attribution works (user came from campaign X)
Content types
- Product page deep link shows correct product
- Search results deep link shows the search with query preserved
- Article deep link scrolls to the article
- Action deep link (e.g., "share settings") opens the right modal
- Authentication-required deep link redirects to login, then back to destination
Auth and session
- Deep link for unauth user → login → lands on original target
- Deep link for logged-out user with cached deep link → login shows "continue to [target]" button
- Expired session deep link → refresh or re-login, preserve destination
- Deep link for different user than logged in → either prompt account switch or reject cleanly
Edge cases
- Malformed URL → lands on home, does not crash
- URL with special characters (emoji, unicode, spaces) → decoded correctly
- Very long URL (2000+ chars) → handled or rejected cleanly
- Path that looks like a deep link but is not mapped → falls back to home or 404 screen
- Deep link with query parameters → all parsed correctly
- Deep link with fragment (
#) → handled - Fake deep link in a malicious notification payload → cannot bypass auth checks
Security
- Deep link cannot bypass auth (visiting
/settings/accountunauthenticated does not show account data) - Deep link cannot cross user contexts (user A's deep link cannot access user B's data)
- Deep link parameters sanitized (SQLi, XSS patterns rejected)
- Path traversal (
../../../etc/passwd) rejected - Third-party app launching your deep link → no privilege escalation
Analytics
- Deep link source tracked (campaign_id, source, medium)
- Attribution propagates to first event after open
- Deep link failures logged (malformed, unmapped, rejected)
Manual testing
Create a test matrix:
- Contexts: Messages, Email (Gmail + Apple Mail), Safari, Chrome, WhatsApp, Slack, another app via share sheet
- States: app installed + foreground, app installed + background, app installed + killed, app not installed
- URLs: valid happy-path link, malformed, expired, unauthenticated-required, very long
Send yourself links in each context. Tap each. Record what happens.
On Android, use adb shell am start -a android.intent.action.VIEW -d "myapp://product/123" to fire a deep link without the source-app overhead. On iOS, xcrun simctl openurl booted "myapp://product/123" for simulator, Safari URL bar for physical.
Automated testing
Android
@Test
fun deepLink_opensProductScreen() {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://product/123"))
val scenario = ActivityScenario.launch<MainActivity>(intent)
onView(withId(R.id.product_title)).check(matches(withText("Product 123")))
}
For App Links, use adb shell am start -W -a android.intent.action.VIEW -d "https://myapp.com/product/123" in an instrumented test and assert the correct activity started.
iOS
func testDeepLink() {
let app = XCUIApplication()
app.launch()
let url = URL(string: "myapp://product/123")!
// Via UIApplication.shared.open or test-time injection
app.openURL(url)
XCTAssertTrue(app.staticTexts["Product 123"].waitForExistence(timeout: 5))
}
End-to-end with Playwright / Appium
Open Safari (or Chrome), type the Universal Link URL, tap the "Open in App" banner if shown, assert app opens to the correct screen.
How SUSA handles deep links
SUSA can launch your app with a deep link intent directly and continue exploration from that entry point:
susatest-agent test myapp.apk --deep-link "myapp://product/123"
susatest-agent test myapp.apk --deep-link "https://myapp.com/order/456"
The adversarial persona tries malformed deep links (injection patterns, traversal, very long paths). Results are tracked per-deep-link: did it navigate to the right screen, was auth enforced, any crashes or errors.
For deferred deep links, you need a dedicated tool (Firebase, Branch) — those are installation-flow-specific and outside SUSA's scope.
Common production bugs
- Deep link lost during cold start — splash activity consumes intent, main activity never sees it. Fix: pass intent explicitly or use a dedicated deep link handler.
- Auth loop — deep link triggers login, login triggers deep link, login triggers... Fix: clear deep link intent after consumption.
- Universal link opens web, not app, on iOS — association file misconfigured or not served with correct content-type. Fix: validate via
swcutiltool. - App Link opens but scheme-only link does not — domain verification fine but intent filter scheme mismatch. Fix: include both schemes in intent filter.
- Deep link triggers crash on old versions — backend updated URL format, app does not recognize. Fix: always degrade gracefully, never crash on unknown paths.
Test deep links every release. The per-context matrix is tedious but catches the bugs that analytics hide.
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