Testing deep links: deep links, Universal Links, App Links and the state matrix
Deep links are what ads, referrals, sharing and push rely on: a link must open the right screen inside the app, not the home screen and not the browser. Sounds simple, but this is exactly where the state matrix hides — the reason for “sometimes it works, sometimes it doesn’t”. Let’s look at how to test deep links properly.
Three link types — and what breaks in each
- Custom scheme (
myapp://route): simple, but if the app isn’t installed, the link leads nowhere (error/blank). Not viable as the only mechanism. - iOS Universal Links (a regular
https://): open the app if installed, otherwise the site. Require anapple-app-site-association(AASA) file on the domain. - Android App Links (also
https://): the Android equivalent, requireassetlinks.jsonand verification. Without verification the system shows a “chooser” dialog or opens the browser.
The state matrix — the main source of bugs
The same deep link must work correctly in all app states:
- App running (foreground) → navigate to the right screen without a reload.
- In the background → resume and navigate to the right place.
- Killed / cold start → launch, and after initialization — navigate (a common bug: the deep link is lost, the home screen opens).
- Not installed → store or web fallback (see deferred below).
The minimum test set for each deep link = 4 states × (valid / broken link). Most bugs are exactly in killed + cold start.
Deferred deep linking
The scenario: a user clicked a link, the app isn’t there → went to the store → after install must land on the screen the link pointed to (a product, promo, group invite). That’s deferred deep linking — navigation “arrives” after install. Verify that:
- The target screen opens on the first launch after installing from the link.
- Attribution is correct: the tracker (Adjust/AppsFlyer) understood which campaign/referral drove it — what deferred deep linking is.
- If the data didn’t arrive — a sensible default screen, not a blank or a crash.
Fallback and broken links
- No app → open the store (the right one for the platform) or the web version of the page.
- A stale/broken link (product deleted, promo ended) → a clear screen (“not found”/section home), not a white screen or a crash.
- An unknown path in the link → graceful handling, default routing.
Security: don’t trust the link blindly
A deep link is user input controlled by anyone. Verify:
- Parameters from the link don’t lead to private screens bypassing authorization (opening
myapp://account/settingswithout login). - No open redirect:
?next=https://evil.comdoesn’t take the user/token outside. - Injections in parameters (id, search) are escaped, don’t break the screen and don’t leak into queries.
- Sensitive actions (payment, changing data) aren’t executed “by link” without confirmation.
Domain verification — why it “opens in the browser”
The classic complaint: “the Universal Link opens the site, not the app.” The cause is almost always the domain association file:
- iOS:
apple-app-site-association(AASA) available athttps://domain/.well-known/apple-app-site-association, served as JSON, no redirects, with the correct appID. See Associated Domains and Universal Links. - Android:
assetlinks.jsonin/.well-known/, App Links verification passed (verify Android App Links, App Links). - Important: a Universal Link must be tapped (from Notes/a messenger), not pasted into the Safari address bar — there it opens as a regular site. That’s not a bug, it’s system behavior.
Attribution and analytics
Marketing pays for clicks, so campaign parameters (utm/referral/ad id) must reach the tracker exactly once and bind to the right source. Verify: duplicate events, parameter loss on cold start, correctness in the deferred scenario.
Tools
- Android:
adb shell am start -W -a android.intent.action.VIEW -d "<url>" <package>— open a deep link on a device/emulator. - iOS simulator:
xcrun simctl openurl booted "<url>". - Checking AASA/assetlinks and redirects — via HTTP interception (Proxyman/Charles): what the domain actually serves.
- A real tap: a link in Notes/chat (not the address bar), on a clean device for deferred.
Deep link checklist (12 points)
- Each deep link is tested in 4 states: foreground / background / killed / not installed.
- Killed + cold start leads to the right screen (not the home screen).
- Deferred: after installing from the link, the target screen opens.
- Campaign/referral attribution is correct and without duplicates.
- Fallback when the app is missing → store/web.
- A broken/stale link → a sensible screen, not a crash.
- The custom scheme isn’t the only mechanism (there are UL/App Links).
- AASA available, JSON, no redirect, correct appID (iOS).
- assetlinks.json in place, App Links verified (Android).
- The link doesn’t open private screens bypassing authorization.
- No open redirect and no injections via parameters.
- Tested on a real tap (Notes/chat), not just from the address bar.
Bottom line
A deep link seems like “just a link,” but its quality is the state matrix × fallbacks × domain verification × security × attribution. Most bugs live in killed + cold start and in deferred, while “it opens in the browser” is almost always an unverified AASA/assetlinks. A systematic state-based checklist catches this before an ad campaign breaks.