Deep Link Testing
A deep link is a link that opens a specific screen inside a mobile app instead of its start screen. Tapping https://shop.example.com/orders/A-1042 in a shipping email should show order A-1042 in the shop app. Links reach users through email, SMS, QR codes, push notifications and other apps, and each one is an entry point that skips the app’s normal navigation.
Deep link testing checks that every such entry point lands on the right screen with the right data in every state the user can be in: app installed or not, running or closed, user signed in, signed out or signed in as someone else. It builds on the mobile scenarios you already know and adds the platform rules that decide whether a link opens the app at all.
Why it matters
Deep links carry the moments that matter: a campaign, a password reset, a delivery update. When one breaks, the failure is quiet. The user lands on the home screen, on a web page that says "not found", or worse on someone else’s data, and the team hears about it through support tickets. Many causes sit outside the app’s code, in a file on the website, a signing certificate or a redirect added by an email tool, so ordinary UI tests that start inside the app do not catch them.
A deep link is also input from outside the app. Anyone can craft one, so the app must validate it and must never skip sign-in or perform an action without confirmation because a link asked it to. For the tester this is a compact, high-value area: a small, well-chosen set of combinations covers the real risks once you know which states and platform rules to combine.
Custom schemes, Android App Links and iOS Universal Links
A custom URL scheme link uses a scheme the app registers for itself, such as shop://orders/A-1042. It is simple, but nothing proves the scheme belongs to your app: another app can register the same one, and when no installed app handles it the link just fails. Keep it for links inside your own products, not for links you send to users.
Android App Links are ordinary https links that Android verifies. The app declares the domain in an intent filter (the manifest entry that lists the links an app can open) with android:autoVerify="true", and the website publishes /.well-known/assetlinks.json with the app’s package name and the SHA-256 fingerprint of its signing certificate. When verification succeeds, the link opens the app directly. Since Android 12, an unverified https link opens in the default browser unless the user has approved the domain for the app in its settings.
iOS Universal Links follow the same idea. The app lists the domain in its Associated Domains entitlement (applinks:shop.example.com), and the website publishes an apple-app-site-association file naming the app and the paths it handles. Since iOS 14, devices fetch this file through Apple’s content delivery network (CDN), which can take up to 24 hours to pick up a change; a device downloads the file when the app is installed and then checks for updates only about once a week, so after a fix, wait for the CDN and reinstall the app on the test device. A universal link typed into Safari’s address bar opens the website, and so does a tap on a same-domain link while the user is already on that site in Safari: platform behaviour, not a defect.
- Both files are served over HTTPS with a valid certificate and without redirects;
assetlinks.jsonasapplication/json. - Every Android build is signed with a key, and the fingerprint identifies that key’s certificate. It must match the key that signed the installed build: debug builds, release builds and builds re-signed by Google Play App Signing have different fingerprints, and the file can list several.
- The iOS file has no
.jsonextension, and its paths apply to every installed version of the app, old ones included. - Deferred deep linking, reaching the target after installing the app from the store, is not a platform feature. If the requirement asks for it, your team or a third-party SDK builds it, and it needs its own tests.
https://shop.example.com/.well-known/assetlinks.json (Android App Links)
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.shop",
"sha256_cert_fingerprints": ["14:6D:E9:...:2E:5F"]
}
}]
https://shop.example.com/.well-known/apple-app-site-association (iOS Universal Links)
{
"applinks": {
"details": [{
"appIDs": ["ABCDE12345.com.example.shop"],
"components": [{ "/": "/orders/*" }]
}]
}
}Installed or not, signed in or not
Test the same link in several states, because each state takes a different path. Not installed: a custom-scheme link fails, while a verified https link opens the website at the same URL, so that URL must be a real page, ideally with a way to get the app, never a 404. Installed but not verified: the website opens although the app is there, the classic sign of a wrong fingerprint or a broken association file. Installed in an older version: the app must fall back to a sensible screen for a path it does not know, not crash.
How the app is running matters too. A link that arrives while the app is closed starts it from scratch; one that arrives while it is in the background or showing another screen reaches different code. Test both, and check where Back leads afterwards.
Then the user. Signed out: sign-in appears and, after it succeeds, the linked screen opens, not the home screen. Signed in as the owner: the right record opens. Signed in as another user: a clear not-available message and none of the owner’s data, enforced by the server and not only hidden by the app. Session expired or first launch after install: re-authentication, onboarding or a permission prompt must not swallow the link.
Answer: A custom scheme such as
shop://Question 2: Is the app installed?Answer: No
Outcome: Nothing opens, or an errorThere is no fallback, so do not send scheme links to usersCautionAnswer: Yes
Outcome: An app opens the targetUnverified: another app can register the same scheme
Answer: An
https://App Link or Universal LinkQuestion 3: Is the app installed?Answer: No
Outcome: The website opens at the same URLThe page must exist and offer the app; never a 404Answer: Yes
Question 4: Did the system verify the domain for this build?assetlinks.jsonon Android,apple-app-site-associationon iOSAnswer: No
Outcome: The website opens although the app is installedA configuration defect: fingerprint, missing file or redirectFailAnswer: Yes
Question 5: Is the user signed in?Answer: No
Outcome: Sign-in, then the linked screenNot the home screenAnswer: Yes
Question 6: May this user see the record?Answer: No
Outcome: A not-available messageNo data from the record; the server refuses the requestAnswer: Yes
Outcome: The linked screen with the right recordPass
The unverified branch follows Android 12 and later; on older Android versions an unverified https link can show a chooser between the app and the browser.
Triggering links and checking verification
Tap links from the channels users actually use: the email app, SMS, a chat app, a QR code scanned with the camera, a push notification. Some apps open links in their own in-app browser, which may not hand them to your app, and email click tracking often wraps a link in a redirect through another domain that is not associated with your app. Test the link exactly as it is sent, not the clean URL from the ticket.
For fast, repeatable checks use the platform tools. On Android, adb fires a link and lets the system choose the handler, and since Android 12 it shows and re-runs domain verification. On the iOS Simulator, simctl opens a URL; on a real iPhone, paste the link into Notes or Messages and tap it.
# Android: fire the link and let the system choose the app (no package name)
adb shell am start -W -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://shop.example.com/orders/A-1042"
# Android 12+: domain verification state, then a fresh verification
adb shell pm get-app-links com.example.shop
adb shell pm verify-app-links --re-verify com.example.shop
# iOS Simulator: open the link through the system
xcrun simctl openurl booted "https://shop.example.com/orders/A-1042"Worked example: the order link in a shipping email
Requirement: "The shipping email links to the order. With the app, the link opens the order details; without it, the order page on the website." The link is https://shop.example.com/orders/A-1042 and the order belongs to Sara. Start from a decision table over four true/false conditions: installed, verified, signed in and owner. Each column of the table is one combination of the four with the screen the link should land on, and the columns give the core cases; the lesson on decision table testing shows how to build one and merge columns where a condition makes no difference. Then add running-state and channel variations to the riskiest columns instead of multiplying everything.
- Installed, Sara signed in, app closed: order A-1042 opens, and Back leads to the orders list as agreed.
- Same, with the app in the background on the cart: order A-1042 opens and the cart is unchanged.
- Installed, signed out: sign-in appears; after Sara signs in, order A-1042 opens, not the home screen.
- Installed, another user signed in: a not-available message, no data from A-1042, and the API refuses the request for that order.
- Not installed: the order page opens on the website (after web sign-in) with a link to the store; no 404.
- A build signed with a key missing from
assetlinks.json: the website opens andpm get-app-linksdoes not show the domain asverified. Report it against the configuration, not the screen. - Unknown, empty or overlong id (
/orders/A-9999,/orders/, 300 characters): a clear error screen, no crash. - The real email, through its click-tracking redirect, in each supported email app: the same result as the clean link.
Try it: Decision Table Generator Try it: Test Case Generator
Common mistakes
- Testing only with the app installed and the user signed in, the one state developers have already tried.
- Firing links only with
adbaimed at the app’s package, or typing them into a browser, which bypasses or changes the platform’s link handling; tap them where users do. - Reporting "the website opened" as an app defect without checking verification first; the cause is usually the association file, the fingerprint or a redirect.
- Accepting the home screen after sign-in as a pass: the user wanted the order, so landing anywhere else is a failure.
- Forgetting older app versions, which still receive links and, on iOS, the current association file.
- Trusting link parameters: a link must never skip sign-in, show another user’s data, or pay, delete or send anything without confirmation.
The QA perspective
Ask early for a deep link inventory: every link the product sends or prints, its URL pattern, the target screen, the fallback and who owns the association files. Agree with developers and the product owner where Back leads after a link, what an unknown path shows and whether deferred deep linking is in scope. Add a check of both association files to the release checklist, because a website deploy can break links without any app change.
In a defect report, record the link exactly as sent, the channel it was tapped from, the device, OS, app version and build, whether the app was installed, running or closed, the signed-in user and the verification state. Probe security behaviour, such as a link that bypasses sign-in, only in apps your team owns or has written permission to test, against the MASVS platform-interaction requirements your team follows.
Practice exercise
Design deep link tests for a promotion SMS
A shop sends the SMS below. Offers are public, but a code can only be applied to a signed-in user’s cart, and an offer can end before the user taps the link.
Design the test cases: the states you combine, the expected result of each, and the checks you run before you blame the app for a failure.
SMS:
Your 10% code is waiting: https://shop.example.com/offers/ramadan?code=SAVE10
Rule:
With the app, the link opens the offer screen and applies the code to the cart.
Without the app, it opens the offer page on the website.- List the states: installed or not, verified or not, app closed or in the background, user signed in or out.
- Write the expected result for each meaningful combination, including what happens to the code.
- Add invalid input: a missing, unknown or lower-case code, and an offer that has ended.
- List the checks that separate a configuration fault from an app defect.
Show the model answer
Installed and signed in, with the app closed and again in the background: the offer screen opens and the cart shows SAVE10 applied once; tapping the link a second time does not apply it twice.
Installed, signed out: the offer opens without sign-in because offers are public. Applying the code asks for sign-in, after which the code is applied and the user is back on the offer or the cart, not the home screen.
Not installed: the website offer page opens with the code, plus a way to get the app; no 404. Keeping the code through a store install is deferred deep linking and needs its own requirement.
Invalid input: no code parameter, an unknown code, save10 in lower case (accepted or rejected as the rules say) and an ended offer each give a clear message; a wrong code never empties the cart.
Installed but not verified: the website offer page opens instead of the app, a configuration fault rather than an app defect. Before you blame the app for this or any other failure, run the checks: tap the link in the SMS app, not the browser; run pm get-app-links on Android; confirm both association files load over HTTPS without redirects, the iOS file covers /offers/* and the Android intent filter declares that path; and trace any link shortener for a redirect through another domain.
Frequently asked questions
What is the difference between a deep link, an App Link and a Universal Link?
A deep link is any link that opens a specific place in an app. Android App Links and iOS Universal Links are deep links on ordinary https URLs whose domain the platform has verified for the app, so they open the app when it is installed and the website otherwise. A custom-scheme link such as shop:// is a deep link without that verification.
Why does the link open the website when the app is installed?
Usually the platform did not verify the domain for this build: the fingerprint in assetlinks.json does not match the signing key, a file is missing, invalid or redirected, or the domain is missing from the intent filter or the associated domains. On iOS it is also expected when the URL was typed into Safari or tapped on a page of the same site. Check verification before you report.
Can I test deep links on emulators and simulators?
Many checks, yes: adb works on the Android emulator and xcrun simctl openurl on the iOS Simulator. Keep at least one pass on real devices with the real channels, such as email apps, in-app browsers, the camera’s QR scanner and store installs, because they behave differently there or exist only there.
Should deep link tests be automated?
The routing, yes: a UI test can open each link pattern on an emulator or simulator and assert the screen, which catches routing regressions on every build, and a script can fetch both association files after each website deploy. Channel behaviour and the not-installed path usually stay manual and run before a release or a campaign.
References
- About App LinksAndroid App Links: deep links to your own website that the system verifies through a Digital Asset Links file (assetlinks.json), so they open the app directly.
- Allowing apps and websites to link to your contentUniversal links on Apple platforms: ordinary web links that open the app when it is installed — once the system has checked a file on your website — and the website otherwise.
- OWASP Mobile Application Security Testing Guide (MASTG)The testing manual that goes with MASVS: techniques and test cases for verifying mobile app security.
- OWASP Mobile Application Security Verification Standard (MASVS)OWASP's security requirements for mobile apps, grouped into control areas such as storage, cryptography, authentication, network and platform interaction.
Practise with Mutqan tools
Build decision tables from conditions and actions, generate all rule combinations and collapse impossible ones.
Break a URL into protocol, host, port, path, query parameters, fragment and credentials.
Validate JSON syntax and get precise error locations with helpful fix suggestions.
Trace redirect chains (301/302/307/308) and see every hop, status and final URL.
Generate structured test cases (id, title, preconditions, steps, expected result, priority) from a feature description and field list.
Generate a minimal pairwise (all-pairs) test set from parameters and their values.
Go deeper
- Mobile App Test ScenariosPush notifications
- Mobile App Test ScenariosInstall, update, uninstall and reinstall
- Mobile Testing: A Practical GuideMobile security: MASVS and MASTG at a glance
- Mobile Test Automation ExplainedHow mobile test automation works: Appium, Espresso and XCUITest compared, accessibility-id locators, sources of flakiness, device farms and CI pipelines.
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.