# How to Test Deep Links on iOS and Android (2026 Guide)

> Test deep links on iOS and Android before shipping: adb commands, xcrun simctl, real-device passes, in-app browser checks, and a full pre-launch QA checklist.

URL: https://u2l.ai/blog/test-deep-links-guide
Published: 2026-10-02T23:19:19+05:30
Updated: 2026-10-02T23:19:19+05:30
Author: Team U2L
Category: developer
Tags: deep-links, testing, qa, universal-links, app-links, developer

---


<!-- TECH_ARTICLE -->
<!-- SOFTWARE_SCHEMA: U2L AI, UtilitiesApplication, Web -->

<!-- SPEAKABLE_START -->
To test deep links properly, verify the association files with curl, launch the link through `adb shell am start` on Android and `xcrun simctl openurl` on the iOS simulator, then tap the same link from Notes, Mail, and SMS on physical devices. Finish with an in-app browser pass (Instagram, Facebook, TikTok) and a no-app-installed run so the fallback is exercised. Skipping any of these misses a real user path.
<!-- SPEAKABLE_END -->

A deep link that opens perfectly on your desk phone is not a deep link that works. Knowing how to test deep links properly means covering iOS 17 and 18, a Pixel and a five-year-old Xiaomi, taps from Instagram DMs and Gmail and the Notes app, and devices that have your app installed alongside devices that never will. Each one is a different code path, and each has to survive the trip from URL to app screen without dumping the user in a browser.

Most "test the deep link" checklists floating around the internet are one bullet long: `adb shell am start`. That command is fine as a sanity check and useless as a QA plan. The real work is stitching together simulator, real-device, network, and human tests into a repeatable pass that catches the failures before your users do. This guide walks through the whole thing: the commands, the tools, the physical device rituals, the in-app browser trap, the CI hook, and the no-code shortcut for links pointing at apps you do not own.

## Table of Contents

- [What "Testing a Deep Link" Actually Means](#what-testing-a-deep-link-actually-means)
- [The Deep Link Testing Matrix](#the-deep-link-testing-matrix)
- [Step 1: Validate the Association Files](#step-1-validate-the-association-files)
- [Step 2: Test on Android with ADB](#step-2-test-on-android-with-adb)
- [Step 3: Test on iOS with Simulator and Safari](#step-3-test-on-ios-with-simulator-and-safari)
- [Step 4: Real-Device QA Pass](#step-4-real-device-qa-pass)
- [Step 5: In-App Browser Test](#step-5-in-app-browser-test)
- [Step 6: The No-App-Installed Path](#step-6-the-no-app-installed-path)
- [Step 7: Automate the Regression Suite](#step-7-automate-the-regression-suite)
- [Testing Deep Links in React Native and Flutter](#testing-deep-links-in-react-native-and-flutter)
- [Pre-Launch Deep Link Checklist](#pre-launch-deep-link-checklist)
- [Frequently Asked Questions](#frequently-asked-questions)

## What "Testing a Deep Link" Actually Means

<!-- DEFINED_TERM: Deep Link Testing -->
**Deep link testing** is the practice of verifying that a URL launches the correct screen inside a mobile app across every combination of device, OS version, hosting context, and install state that your users will actually encounter, and that it fails gracefully when the app is missing.
<!-- DEFINED_TERM_END -->

That definition matters, because a lot of QA plans confuse "the link works on my phone" with "the link works." Deep linking sits at the intersection of four moving parts: the URL string itself, the operating system's link association files (AASA for iOS, assetlinks.json for Android), the hosting context that received the tap (Safari, Gmail, Instagram's WebView), and the device's cached state. Any one of them can silently break the handoff, and none of them will send you a helpful error.

So the goal of a proper test pass is not "launch the app once." It is to prove the link survives:

- iOS and Android, on more than one OS version each
- Fresh installs, app updates, and no-install states
- System contexts (Notes, Mail, Messages) and in-app browsers (Instagram, Facebook, TikTok)
- Query parameters, unicode, and edge cases in your URL parser
- The exact URL shape your marketing team will actually ship, including any tracking wrappers

If your test only proves "iMessage → app on my iPhone," you have covered maybe fifteen percent of the real distribution. The rest of this guide is about the other eighty-five.

If you want the setup context that precedes testing, our [iOS Universal Links guide](/blog/universal-links-ios-guide) and [Android App Links guide](/blog/android-app-links-guide) walk through the config side, and the [mobile deep linking pillar guide](/blog/mobile-deep-linking-guide) sets the vocabulary for how deep links, universal links, and app openers all fit together.

## The Deep Link Testing Matrix

Before running commands, sketch the matrix. Every deep link has these axes, and you want at least one test cell in each row:

| Axis | Values to cover |
|---|---|
| Platform | iOS + Android (both, always) |
| OS version | Current + one prior major (e.g. iOS 17 + 18, Android 13 + 14) |
| Device | At least one physical device per platform |
| Install state | App installed, app not installed, app updated over old version |
| Tap context | System app (Notes/Mail/SMS), in-app browser (Instagram/Facebook), web browser (Safari/Chrome) |
| Network | Wi-Fi and cellular (some CDNs behave differently) |
| URL variant | Base URL + one with query params + one with encoded characters |

You are not testing every cell of the cross product; that is thousands of runs. You are testing enough to prove each axis holds up. A minimum viable pass is roughly 20 taps, and it takes about 30 minutes on a good day. Anything less and you are gambling.

## Step 1: Validate the Association Files

Deep link testing starts on the server, not the phone, because a broken association file makes every downstream test moot. Two files matter: `apple-app-site-association` for iOS, `assetlinks.json` for Android. Both live under `/.well-known/` on your domain, both must be served over HTTPS with `Content-Type: application/json`, and neither is allowed to sit behind a redirect.

**Check iOS:**

```
curl -sI https://yourdomain.com/.well-known/apple-app-site-association
```

You want HTTP 200, `content-type: application/json`, and no `location:` header. Also check what Apple's CDN has cached, because that is the copy your users' devices actually consume:

```
curl -s https://app-site-association.cdn-apple.com/a/v1/yourdomain.com
```

If that returns your latest file, you are propagated. If it returns yesterday's file, wait; Apple caches aggressively and there is no cache-bust button. Apple's own [Supporting Associated Domains documentation](https://developer.apple.com/documentation/xcode/supporting-associated-domains) covers the CDN behavior.

**Check Android:**

```
curl -sI https://yourdomain.com/.well-known/assetlinks.json
```

Same requirements: 200, JSON content type, no redirects. Then run Google's [Statement List Tester](https://developers.google.com/digital-asset-links/tools/generator) against the file to confirm the SHA-256 fingerprint matches your Play Console signing key, not your upload key. This is the number one reason Android App Links fail after launch.

Both files must be valid before you touch a device. If either one is broken, every subsequent test will fail for the same reason and you will spend an afternoon "debugging" a link that is fine.

## Step 2: Test on Android with ADB

Android's activity manager gives you the cleanest deep link test on the planet: you hand the URL directly to the intent system and see exactly what happens, with no in-app browser, no redirect chain, and no user gesture to blame.

Connect a device or start an emulator, then run:

```
adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/42" com.yourcompany.app
```

The `-W` flag makes `am` wait for the launch to complete and print the result, which is genuinely useful; if you see `Status: ok` and your activity in the output, the intent resolved. Google's [deep linking guide](https://developer.android.com/training/app-links/deep-linking) documents the full syntax.

Pass the package name only when you want to force your app to receive the intent (useful during development). Drop the package name to see what Android actually resolves the URL to in production; if the browser opens instead of your app, verification failed.

Follow up with:

```
adb shell pm get-app-links com.yourcompany.app
```

You want `verified` next to every domain. If any say `1024` or `legacy_failure`, run:

```
adb shell pm verify-app-links --re-verify com.yourcompany.app
```

And re-check. Verification runs at install time, so a fixed server does nothing for installed devices until you re-verify.

Two ADB tricks worth banking:

- **Test custom schemes** the same way with `-d "myapp://path"`. If nothing happens, your intent filter is off.
- **Simulate the app being backgrounded** by pressing home before firing the intent. Some deep link handlers only run on cold start and silently ignore warm launches.

## Step 3: Test on iOS with Simulator and Safari

iOS testing is fussier than Android testing, partly because the simulator is not fully trusted for universal links and partly because Apple has removed several of the historically useful hooks. In 2026, the reliable playbook is a combination of simulator smoke tests, Safari sanity checks, and physical-device confirmation.

**In the simulator:**

```
xcrun simctl openurl booted "https://yourdomain.com/product/42?ref=test"
```

This is the closest thing iOS has to `adb shell am start`. Quote the URL if it contains query parameters, or the shell will eat everything after the ampersand. If your app opens to the right screen, the URL parser and routing are healthy. If Safari opens instead, either the entitlement is missing from your build or the simulator has not fetched your AASA (see the associated-domains note below).

Do NOT type the URL into Safari's address bar and press return on iOS. Apple deliberately made that path never trigger a universal link, to prevent apps from hijacking search results. The correct Safari test is to load any other page and follow a hyperlink to the URL, or to load the URL in a page that has an `<a href>` you can tap. Failing that, drop the link into Notes, save, and tap it there; Notes taps behave like Messages taps.

**Physical device:**

Universal links only fully behave on real hardware after the app has been reinstalled at least once since the AASA changed. The trick most teams miss is that installing a debug build with `applinks:yourdomain.com?mode=developer` in the entitlement makes iOS bypass the CDN and fetch your AASA directly. Use this during config work; ship without it.

For the AASA cache specifically, Apple provides a system endpoint to inspect what your device fetched:

```
Settings → Developer → Universal Links → Diagnostics
```

That screen appears only on iOS devices with a developer profile installed, and it tells you exactly which domains iOS considers "associated" and why any missing ones failed. If your domain is not in the list, testing anything else is a waste of time; fix the association first.

## Step 4: Real-Device QA Pass

A real user opens links from at least five different contexts on any given day: Messages, Mail, Slack, the browser they use, and whichever social app they live in. Your QA has to cover the same surface, on real hardware, because every context handles link taps differently.

Run this ritual on one iPhone and one Android device, both connected to Wi-Fi:

1. Send the deep link to yourself over iMessage/SMS and tap it. The app should open to the exact target screen.
2. Send it over email and tap from Mail/Gmail. Same expected result.
3. Paste it into Notes/Google Keep, save, tap. This confirms the "clean" system-context path.
4. Paste it into Slack DM to yourself, tap. Slack has its own preview scraper that can interact badly with association files.
5. Load the URL in Safari/Chrome (as a hyperlink on some page, not typed) and tap.
6. Force-quit the app and tap the link again; some deep link routers only run correctly on warm launches. Some only run on cold launches. Both matter.
7. Kill airplane mode and re-run the taps on cellular. Some CDNs return different responses to mobile carriers, which occasionally breaks AASA fetches.

Every one of those steps should land on the exact target screen with the parameters preserved. If step 5 opens the browser instead of the app on iOS, the tap came from a context that does not honor universal links (typed URL). If step 4 opens Slack's in-app browser, you are already learning what Step 5 of the next section is about.

Log the failures with context: OS version, device, source app, whether the target app was already running. Deep link bugs almost always cluster on one axis, and the cluster is your root cause.

## Step 5: In-App Browser Test

<!-- CLAIM: A universal link that works from Notes will always work from Instagram -->
<!-- CLAIM_RATING: False -->
<!-- CLAIM_EXPLANATION: Instagram, Facebook, TikTok, and most in-app browsers intercept link taps before iOS or Android sees them, and load the destination inside their embedded WebView instead of handing the URL off to the operating system for app resolution. A perfectly configured universal link will render as a web page in these contexts by default. -->

This is the single most-skipped step in deep link QA, and it fails more real user taps than every other step combined. Half of the URLs you ship land in Instagram DMs, Facebook posts, TikTok comments, and X links, and every one of those apps intercepts taps in their own WebView instead of asking the OS to resolve the URL. Universal links do not fire. App Links do not fire. The user sees a stripped-down web page while your app sits closed in the background.

Test it explicitly. Post the link in a story on Instagram, share it in a Facebook DM to yourself, drop it in a TikTok bio, and tap each one on both platforms. Confirm the destination. If it renders in the in-app browser instead of opening your app (or the target app you are linking to), you have three choices:

1. Ship a version of the link that includes the `&_branch_match_id=` style parameter your SDK generates to force an app open. Works only if you own the target app.
2. Wrap the link in an [app opener](/blog/what-is-an-app-opener) that detects the hosting app and escapes the WebView. This is the standard fix for links pointing at apps you do not own.
3. Educate the user to tap the three dots and choose "Open in browser," then rely on the OS. Not really a fix.

For links into third-party apps (YouTube, Amazon, Spotify, WhatsApp), U2L AI's app opener handles the escape automatically. Create the link at u2l.ai, share the u2l.ai URL, and it launches the correct app from inside Instagram or Facebook without any config on your side. That is a genuinely nice property, and it is why every serious link into an app you do not own should go through a router rather than a raw URL. Our explainer on [why links open in the in-app browser](/blog/why-links-open-in-app-browser) covers the mechanics.

## Step 6: The No-App-Installed Path

Half of your users will tap a deep link without the target app installed. What happens next is entirely determined by how the link was built, and it is where most launches ship a bad experience without knowing.

Test it. On a spare device or in the simulator, uninstall the app and tap the link. There are three acceptable outcomes:

- **Web fallback:** the link loads the equivalent web page. Fine when the web version delivers value.
- **Store fallback:** the link routes to the App Store or Play Store listing. Fine when the destination only exists in-app.
- **Deferred deep link:** the link loads a landing page, prompts install, and after first launch the app opens to the original target screen. This is the gold standard for install campaigns, and our [deferred deep linking explainer](/blog/what-is-deferred-deep-linking) walks through how the state gets carried across the store.

The unacceptable outcomes: an "address invalid" error (custom scheme with no fallback), a blank page, a 404, or a store listing when the user was mid-flow on your marketing site. Any of those means the link ships broken for people who most need it to convert.

Run the no-install path once per platform. It takes two minutes and prevents the entire class of "we launched a campaign and half the taps went nowhere" postmortems.

## Step 7: Automate the Regression Suite

Manual testing works for the pre-launch pass. It does not work as a permanent safety net; deep links break silently every time you ship a new build, rotate a signing key, migrate a CDN, or let marketing wrap a link in a new tracker. A ten-minute CI check catches most of it.

The minimum viable automation is:

1. A cron job (GitHub Actions, GitLab CI, whatever you use) that runs the association-file curl checks from Step 1 daily. If AASA disappears or starts returning `text/html`, page someone.
2. A build-time test suite that runs `xcrun simctl openurl` and `adb shell am start` against a matrix of representative URLs in the simulator/emulator, using Espresso, XCUITest, or Detox to assert the correct screen opened.
3. A dogfood loop that includes at least one real-device test in the release candidate pipeline. Firebase Test Lab and BrowserStack both host physical devices that respond to shell commands, so you do not need a farm of your own.

For teams shipping React Native or Flutter, the same pattern applies but the assertion happens at the JS/Dart layer: after the intent fires, wait for the deep link handler to be called and assert the parsed route matches. Every framework has a testing shim for this; use it.

We use exactly this pattern at U2L AI. Every push runs the AASA/assetlinks curl checks, and any change to redirect logic or the app opener router triggers a full simulator matrix against 30+ representative URLs. Deep link regressions are the class of bug where "we noticed after launch" costs orders of magnitude more than "we noticed in CI," and any automation you can bolt on pays for itself the first time it catches a bad deploy.

## Testing Deep Links in React Native and Flutter

Cross-platform frameworks add a layer between the OS intent and your route handler, and that layer is where a lot of deep link tests miss. The commands are the same (`adb`, `xcrun simctl`), but the assertion is different: you are checking that the framework surfaced the URL to your JS or Dart code with the parameters intact.

**React Native.** Use `Linking.getInitialURL()` for cold starts and the `url` event for warm starts. Test both explicitly: fire `xcrun simctl openurl` with the app killed and with the app backgrounded, and assert the same handler ran with the same parsed route. React Navigation and Expo Router both provide test utilities for the deep link resolver; run them in Jest alongside the manual pass.

**Flutter.** The `uni_links` and `app_links` packages surface the URL to Dart via a stream. Deep link handlers registered before `runApp` catch cold-start URLs; anything registered later misses them. In tests, mock the platform channel and pump the same URL matrix through it, asserting the router state matches expected. Then run the same URLs through the platform via ADB/simctl to confirm the channel actually fires.

Both frameworks have a specific gotcha: hot reload does not re-run the cold-start deep link handler. If a URL only works after a full uninstall/reinstall, that is the bug, not a test artifact. Reinstall, run the URL, and see it fail cleanly before you assume the framework is smart.

## Pre-Launch Deep Link Checklist

Before any campaign goes live, walk this list end to end. It takes 30 minutes on a first run and 15 on subsequent runs, and it is cheap insurance against the deep link failure modes that are otherwise invisible until a user complains.

- [ ] `curl -sI` on the AASA and assetlinks.json paths returns 200 + JSON + no redirects
- [ ] `app-site-association.cdn-apple.com/a/v1/yourdomain.com` returns the current AASA
- [ ] Google's Statement List Tester passes for the assetlinks fingerprint
- [ ] `adb shell pm get-app-links` shows `verified` for every declared Android domain
- [ ] `adb shell am start` launches the app to the correct screen for base URL + parameterized URL
- [ ] `xcrun simctl openurl` launches the app in the simulator to the correct screen
- [ ] Physical iPhone tap from Messages, Mail, Notes → app opens to the correct screen
- [ ] Physical Android tap from SMS, Gmail, Keep → app opens to the correct screen
- [ ] Tap from Instagram DM → app opens (or the app opener escape route runs)
- [ ] Tap from Facebook post → app opens (or the app opener escape route runs)
- [ ] No-app-installed tap on both platforms lands on the intended fallback
- [ ] CI has a scheduled association-file health check
- [ ] Release pipeline runs a simulator matrix against the deep link routes

Miss any of these and you are shipping without knowing whether a real user path is intact. Hit all of them and the class of "the link is broken for some people" bug basically stops existing.

If your links point at apps you do not own (any social app, Amazon, YouTube, Spotify, streaming services), most of the AASA/assetlinks steps are not yours to control. Skip to the in-app browser and no-install tests, wrap the link in [U2L AI's app opener](/app-opener), and the association layer is handled for you. For a broader look at options, our roundup of [the best deep link generators](/blog/best-deep-link-generators) compares no-code routers and SDK-based platforms side by side.

## Frequently Asked Questions

### How do I test a deep link on Android?
Use `adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/path" com.yourpackage.app` from a terminal with a device or emulator connected. The `-W` flag returns the launch status so you can see whether the intent resolved. Follow up with `adb shell pm get-app-links com.yourpackage.app` to confirm every domain reports `verified`; unverified domains will always route to the browser, no matter how correct the URL looks.

### How do I test a universal link on iOS?
Use `xcrun simctl openurl booted "https://yourdomain.com/path"` in the simulator for a first sanity check, then move to a physical device and tap the same link from Notes, Mail, and Messages. Do not type the URL into Safari's address bar; Apple deliberately made that path never trigger a universal link, so it will always open Safari and mislead you into thinking the association is broken.

### Can I test deep links in the iOS simulator?
Partly. `xcrun simctl openurl` fires the URL and lets you confirm the routing code runs, but universal links do not always resolve reliably in the simulator because AASA fetching behaves differently there. Use the simulator for smoke tests and the routing layer, and always confirm on a real device before shipping.

### Why does my deep link work in Notes but not in Instagram?
Instagram, Facebook, TikTok, and most social apps intercept link taps in their embedded WebView instead of handing the URL to the operating system. Universal links and App Links only fire when the OS sees the tap, so a link that opens the app cleanly from Notes will render as a web page inside Instagram. Wrap the link in an app opener that detects the hosting app and escapes the WebView, or accept that social-shared links will not app-launch by default.

### How do I test what happens if the app is not installed?
Uninstall the target app (or use a spare device that never had it) and tap the link. Confirm the fallback path lands on the intended destination: your web page, the App Store/Play Store listing, or a deferred deep link landing page. If the tap produces an "address invalid" error or a blank page, your fallback strategy is broken and you need to fix it before launch, not after.

### What tools help me test deep links besides adb and simctl?
Apple's [Associated Domains diagnostic](https://developer.apple.com/documentation/xcode/supporting-associated-domains) (Settings → Developer → Universal Links on a device with a developer profile), Google's [Statement List Tester](https://developers.google.com/digital-asset-links/tools/generator), BrowserStack and Firebase Test Lab for real-device automation, and Xcode/Android Studio's built-in App Links assistants. For links pointing at apps you do not own, tools like U2L AI's app opener test the app-launch behavior end to end.

### How often should I re-test my deep links?
Every app release, every OS beta, every signing key rotation, every CDN migration, and every time marketing adds a new tracking wrapper to campaign URLs. A cron job that runs the association-file health check daily catches the silent server-side breakages; a simulator matrix on every release candidate catches the client-side regressions. Anything less and you will find out from a user support ticket.

### Do I need to test deep links on multiple OS versions?
Yes. Apple and Google both change deep link behavior between major releases; iOS 14 added the CDN cache, iOS 17 changed AASA handling, Android 12 made App Links verification mandatory, and Android 14 further tightened intent filters. Cover the current major version and one prior version on each platform to catch behavioral shifts before your users do.

## Test It Before Your Users Do

Deep link testing is boring, repetitive, and completely non-negotiable. The links that carry your best-converting campaigns are the same links that break silently every time something upstream changes, and the users who hit those broken links do not tell you; they just do not come back. If you own the target app, run the checklist above before every launch. If you are linking into apps you do not own, [create a free U2L AI account](https://u2l.ai/app/signup), turn any URL into an app opener with [analytics built in](/features), and inherit the testing we already do on our end.

<!-- ABOUT: Mobile deep linking, https://en.wikipedia.org/wiki/Mobile_deep_linking -->
<!-- ABOUT: Universal Links, https://developer.apple.com/ios/universal-links/ -->
<!-- MENTIONS: Apple, https://www.apple.com -->
<!-- MENTIONS: Google, https://www.google.com -->
<!-- MENTIONS: Android, https://www.android.com -->
<!-- MENTIONS: Xcode, https://developer.apple.com/xcode/ -->
<!-- MENTIONS: Android Studio, https://developer.android.com/studio -->
<!-- MENTIONS: Instagram, https://www.instagram.com -->
<!-- MENTIONS: Facebook, https://www.facebook.com -->
<!-- MENTIONS: TikTok, https://www.tiktok.com -->
<!-- MENTIONS: BrowserStack, https://www.browserstack.com -->
<!-- MENTIONS: Firebase Test Lab, https://firebase.google.com/docs/test-lab -->
<!-- MENTIONS: React Native, https://reactnative.dev -->
<!-- MENTIONS: Flutter, https://flutter.dev -->
