How to Test Deep Links on iOS (Complete Guide)

Deep links are fundamental to modern iOS application user experiences, allowing direct navigation to specific content or features within an app rather than just launching the app itself. This article

January 31, 2026 · 15 min read · How-To Guides

Understanding Deep Links and Their Criticality in iOS Testing

Deep links are fundamental to modern iOS application user experiences, allowing direct navigation to specific content or features within an app rather than just launching the app itself. This article provides a comprehensive guide on How to Test Deep Links on iOS, covering everything from understanding their mechanics to implementing robust manual and automated testing strategies. Properly implemented deep links enhance user engagement, improve conversion rates for marketing campaigns, and provide seamless integration with other applications or web content. Conversely, broken deep links lead to frustrating user experiences, lost revenue, and damage to an app's reputation. Imagine a user tapping a promotional offer link from an email, only to land on the app's home screen instead of the discounted product page – a common failure mode for inadequately tested deep links. Our goal here is to equip you with the knowledge and practical techniques to prevent such issues and ensure your iOS deep links function flawlessly across all scenarios.

Why Deep Links Matter and What Breaks in Production

Deep links are not just a convenience; they are a critical component of user journeys, especially in complex applications. They bridge the gap between external touchpoints (emails, marketing campaigns, social media posts, web pages) and internal app content. For instance, an e-commerce app uses deep links to guide users directly to a specific product, a news app to an article, or a social media app to a user profile.

What often breaks in production, leading to a poor user experience, includes:

Understanding these common failure points is the first step toward designing a comprehensive deep link testing strategy.

Anatomy of iOS Deep Linking: URL Schemes vs. Universal Links

Before diving into testing, it's crucial to understand the two primary mechanisms for deep linking on iOS: URL Schemes and Universal Links. Each has its own characteristics, implementation details, and, consequently, its own testing considerations.

URL Schemes

URL Schemes are the older, more traditional method. They allow you to define custom URLs that your app can respond to, much like http:// or mailto://.

Example: myapp://product?id=12345

Implementation:

  1. Define in Info.plist: You declare your custom URL scheme (e.g., myapp) in your app's Info.plist file under "URL Types."
  2. Handle in AppDelegate/SceneDelegate: Your app's delegate methods, specifically application(_:open:options:) for AppDelegate or scene(_:openURLContexts:) for SceneDelegate (for SwiftUI/iOS 13+), receive the incoming URL and parse it to determine the destination.

Pros:

Cons:

Universal Links

Universal Links, introduced in iOS 9, are Apple's recommended approach. They are standard HTTP/HTTPS links that can simultaneously function as deep links to your app and as regular web links.

Example: https://www.susatest.com/product?id=12345

Implementation:

  1. Associated Domains Entitlement: Your Xcode project must include the Associated Domains capability, specifying the domains your app can handle (e.g., applinks:www.susatest.com).
  2. apple-app-site-association File: A JSON file named apple-app-site-association (without a .json extension) must be hosted at the root or .well-known directory of your web server (e.g., https://www.susatest.com/apple-app-site-association). This file maps URL paths on your domain to specific app identifiers and specifies which paths your app should handle.
  3. Handle in AppDelegate/SceneDelegate: Your app's delegate methods, specifically application(_:continue:restorationHandler:) for AppDelegate or scene(_:continue:) for SceneDelegate, receive the NSUserActivity containing the Universal Link.

Pros:

Cons:

Comparison Table: URL Schemes vs. Universal Links

FeatureURL SchemesUniversal Links
IntroducediOS 2.0 (early iPhone OS)iOS 9.0
URL FormatCustom scheme (e.g., myapp://path)Standard HTTP/HTTPS (e.g., https://domain.com/path)
Fallback if App Not Inst.Fails (error message or nothing)Opens in Safari to the corresponding web page
Security/CollisionProne to collisions; less secureUnique to your domain; secure
Setup ComplexityLow (app-side only)Moderate to High (app-side + web server config)
User ExperienceCan be clunky; interrupts flowSeamless; smooth transition
When to UseLegacy apps, internal app-to-app communicationPreferred for external deep links, marketing, web integration

For any new development or significant deep linking implementation, Universal Links should be the default choice due with their superior user experience and security. URL Schemes are mostly for backward compatibility or very specific internal app-to-app communication where Universal Links might be overkill.

Designing a Comprehensive Deep Link Test Matrix

A thorough test matrix for deep links ensures coverage of happy paths, error conditions, edge cases, and non-functional requirements. This structured approach helps identify bugs early and reduces the chance of production issues.

Core Deep Link Test Cases

This table outlines the essential test cases applicable to both URL Schemes and Universal Links.

Test Case IDScenario DescriptionExpected BehaviorParameters/Context
DL-001Happy Path: Basic NavigationApp opens, navigates directly to the specified screen.myapp://product?id=123 / https://domain.com/product/123
DL-002Happy Path: With Multiple ParametersApp opens, navigates to screen, all parameters are correctly applied.myapp://search?query=shoes&color=red
DL-003App in Background: Resume NavigationApp resumes from background, navigates to the specified screen.Same as DL-001
DL-004App Not Running: Cold Start NavigationApp launches from cold start, navigates to the specified screen.Same as DL-001
DL-005Invalid Scheme/Domain (Universal Links)If Universal Link, opens in Safari. If URL Scheme, fails to open.myapp-typo://product / https://wrongdomain.com/product
DL-006Invalid Path (Universal Links)Opens in Safari (if Universal Link), or app opens to default.https://domain.com/nonexistent-path
DL-007Missing Required ParameterApp navigates to screen, but shows error or generic state.myapp://product (missing id)
DL-008Invalid Parameter Value (e.g., non-numeric ID)App navigates to screen, handles invalid value gracefully (error/default).myapp://product?id=abc
DL-009Deep Link to Authenticated Content (Logged In)App navigates directly to content.myapp://profile (user logged in)
DL-010Deep Link to Authenticated Content (Not Logged In)App prompts for login, then navigates to content post-login.myapp://profile (user logged out)
DL-011Deep Link with URL Encoded CharactersApp decodes parameters correctly, navigates.myapp://search?q=t-shirt%20red
DL-012Deep Link with Special CharactersApp handles parameters with & or other special chars correctly.myapp://promo?code=SAVE%26WIN
DL-013Deep Link to Tabbed Interface (Specific Tab)App opens to the correct tab within a tab bar controller.myapp://dashboard?tab=analytics
DL-014Deep Link to Nested NavigationApp navigates through multiple levels of a navigation stack.myapp://category/electronics/product/123
DL-015Deep Link followed by Back NavigationUser can navigate back through the app's stack correctly after deep link.myapp://product/123 -> Back button -> Previous screen.

Advanced Scenarios and Edge Cases

These tests delve into more complex interactions and potential pitfalls.

Test Case IDScenario DescriptionExpected BehaviorParameters/Context
DL-016Universal Link: App Not Installed (Deferred)Opens in Safari, ideally triggers app store, then deep links post-install.https://domain.com/product/123 (app not installed)
DL-017Universal Link: apple-app-site-association MismatchOpens in Safari, does *not* open app. Indicates server config issue.https://domain.com/product/123 (wrong AASA file)
DL-018Multiple Deep Links in Quick SuccessionApp processes the *last* deep link, or handles queue gracefully.Two links tapped rapidly.
DL-019Deep Link while App is Presenting Modal/AlertDeep link is handled after modal/alert is dismissed, or deferred.myapp://home while alert is active.
DL-020Deep Link to Non-existent Content (ID)App shows "content not found" or appropriate error.myapp://product?id=99999 (non-existent)
DL-021Accessibility: VoiceOver InteractionDeep linked screen is fully accessible and navigable with VoiceOver.Any deep link, with VoiceOver enabled.
DL-022Security: Malformed URLs/Injection AttemptsApp safely strips/ignores malicious payload, prevents injection.myapp://product?id=123;DROP TABLE Users;
DL-023Security: Excessive Data in ParamsApp handles large parameter values without crashing.myapp://data?payload=long_string_of_gibberish...
DL-024Deep Link from Different SourcesConsistent behavior whether from Safari, Mail, Messages, other apps.Test from various sources.
DL-025Deep Link to expired/unavailable contentApp displays appropriate message (e.g., "Item no longer available").myapp://offer?id=expired_promo

This matrix provides a solid foundation. Remember to adapt it to your specific app's features, navigation structure, and deep linking requirements.

Manual Testing of iOS Deep Links

Manual testing is indispensable for deep links, especially for verifying the end-user experience, visual correctness, and subtle navigational behaviors that automated tests might miss.

Prerequisites for Manual Testing

Before you begin, ensure you have:

Step-by-Step Manual Testing Procedures

#### 1. Testing URL Schemes

This is relatively straightforward as it doesn't involve web server interaction.

Method 1: Using Safari (Simplest)

  1. Open Safari on your iOS device or simulator.
  2. In the URL bar, type your deep link: myapp://product?id=123.
  3. Press Go.
  4. Expected: A prompt "Open in 'YourApp'?" (or similar) appears. Tap "Open." The app should launch (or come to foreground) and navigate to the specified content.
  5. Verify: Check the navigated screen, parameters, and overall app state.

Method 2: Using the xcrun simctl openurl Command (Simulator Only)

  1. Ensure your simulator is running.
  2. Open your terminal.
  3. Execute the command:
  4. 
        xcrun simctl openurl booted "myapp://product?id=123"
    

(Replace booted with your simulator's UDID if multiple are running).

  1. Expected: The app should launch/foreground on the simulator and navigate.
  2. Verify: Check the navigated screen, parameters, and overall app state.

Method 3: From Another App (e.g., Mail, Notes)

  1. Paste your deep link myapp://product?id=123 into an email draft, a note, or a message.
  2. Tap on the link within that app.
  3. Expected: Similar to Safari, a prompt to open your app, then navigation.
  4. Verify: Ensure the experience is smooth across different source apps.

#### 2. Testing Universal Links

Universal Links require more careful setup and testing due to their reliance on the apple-app-site-association file and Safari's behavior.

Method 1: From Safari (Initial Check)

  1. Open Safari on your iOS device or simulator.
  2. Type your Universal Link: https://www.susatest.com/product/123.
  3. Press Go.
  4. Expected (First Time/App Not Installed): The web page (corresponding to https://www.susatest.com/product/123) should open in Safari. Importantly, a small banner "Open" or "Open in 'YourApp'" should appear at the top of the Safari browser (if the apple-app-site-association is correctly configured and the app is installed).
  5. Expected (Subsequent Times/App Installed & Banner Tapped): If you tap the "Open" banner *once*, subsequent taps on the same Universal Link *from outside Safari* (e.g., Mail, Messages) should open directly in your app. If you tap the link *from Safari itself*, it might still open in Safari or open directly in the app depending on internal Safari heuristics. This is a common source of confusion.
  6. Verify:

Method 2: From Another App (Crucial for Universal Links)

  1. Paste your Universal Link https://www.susatest.com/product/123 into an email, message, or note.
  2. Tap on the link.
  3. Expected (App Installed): The app should open *directly* to the specified content, *without* going through Safari. This is the primary benefit and a key test of Universal Links.
  4. Expected (App Not Installed): The link should open in Safari to the corresponding web page.
  5. Verify: This is where Universal Links truly shine. If it opens Safari first, then you have to tap a banner, something is likely misconfigured. Common issues include:

Method 3: Using the xcrun simctl openurl Command (Simulator Only)

  1. Ensure your simulator is running.
  2. Open your terminal.
  3. Execute the command:
  4. 
        xcrun simctl openurl booted "https://www.susatest.com/product/123"
    
  5. Expected: The app should launch/foreground on the simulator and navigate to the specified content *directly*, bypassing Safari. This command simulates tapping a Universal Link from an external source.
  6. Verify: Check the navigated screen, parameters, and overall app state.

Checklist for Manual Deep Link Testing

Manual testing is time-consuming but essential for catching nuanced issues. It also serves as a critical baseline before investing in automation.

Automated Testing Approaches for iOS Deep Links

Automating deep link testing significantly boosts efficiency and coverage, especially for regression testing. We'll explore several approaches, focusing on iOS-specific tools and frameworks.

1. XCUITest (Apple's UI Testing Framework)

XCUITest, integrated into Xcode, is ideal for UI and integration testing on iOS. It can simulate deep link activations and verify the resulting UI state.

Key Concepts:

Example: Testing a URL Scheme with XCUITest

Let's assume your app handles myapp://product?id=123.


import XCTest

class DeepLinkTests: XCTestCase {

    var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch() // Launch the app normally first
    }

    override func tearDownWithError() throws {
        app.terminate()
    }

    func testProductDeepLink() throws {
        // Simulate opening the URL scheme
        let url = URL(string: "myapp://product?id=123")!
        app.open(url)

        // Give the app some time to process the deep link and navigate
        let productScreen = app.staticTexts["Product ID: 123"]
        XCTAssertTrue(productScreen.waitForExistence(timeout: 5), "Product screen with ID 123 did not appear.")

        // Further assertions: check other UI elements, back button, etc.
        let backButton = app.navigationBars.buttons["Back"]
        XCTAssertTrue(backButton.exists)
        backButton.tap()
        
        let homeScreen = app.staticTexts["Home Screen"] // Assuming a home screen element
        XCTAssertTrue(homeScreen.waitForExistence(timeout: 3), "Did not navigate back to Home Screen.")
    }

    func testDeepLinkToInvalidProduct() throws {
        let url = URL(string: "myapp://product?id=99999")!
        app.open(url)

        let errorText = app.staticTexts["Product not found"]
        XCTAssertTrue(errorText.waitForExistence(timeout: 5), "Error message for invalid product did not appear.")
    }
    
    // You can test different app states by restarting the app
    func testColdStartDeepLink() throws {
        app.terminate() // Ensure app is not running
        
        let url = URL(string: "myapp://settings")!
        app.open(url)
        
        let settingsScreen = app.staticTexts["Settings"]
        XCTAssertTrue(settingsScreen.waitForExistence(timeout: 5), "Settings screen did not appear on cold start.")
    }
}

Testing Universal Links with XCUITest:

The app.open(url) method works for Universal Links as well, as it simulates the OS handling the URL. The key is to ensure your apple-app-site-association setup is correct on your test environment's web server. XCUITest will directly open the app if the Universal Link is properly configured and the app is installed.

2. Appium (Cross-Platform Automation)

Appium is a popular open-source tool for automating mobile apps, including iOS. It allows you to write tests in various languages (Java, Python, C#, JavaScript, Ruby) and interact with the app's UI elements.

Key Concept for Deep Links:

Appium allows you to start an application with a deep link URL directly.

Example: Testing a Universal Link with Appium (Python)

First, ensure you have Appium server running and appium-doctor shows no issues. You'll need webdriver and appium-python-client.


from appium import webdriver
from appium.options.ios import XCUITestOptions
from appium.webdriver.common.appiumby import AppiumBy
import time

# Desired Capabilities for your iOS app
# Replace with your app's bundle ID and device/simulator details
options = XCUITestOptions()
options.platform_name = "iOS"
options.platform_version = "17.4" # Or your target iOS version
options.device_name = "iPhone 15 Pro Max" # Or your target device/simulator
options.bundle_id = "com.yourcompany.YourApp"
options.automation_name = "XCUITest"

# This is the key: set the 'app' capability to your deep link URL
# For Universal Links, use the HTTP/HTTPS URL
# For URL Schemes, use the custom scheme URL
options.app = "https://www.susatest.com/product/456" # Or "myapp://product?id=456"

class DeepLinkAppiumTests:

    def setUp(self):
        self.driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
        self.driver.implicitly_wait(10)

    def tearDown(self):
        if self.driver:
            self.driver.quit()

    def test_universal_link_to_product(self):
        try:
            # Verify that the correct screen is displayed
            product_title = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Product ID: 456")
            self.assertTrue(product_title.is_displayed(), "Product screen for ID 456 not displayed.")

            # Perform further actions/assertions on the screen
            add_to_cart_button = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Add to Cart")
            self.assertTrue(add_to_cart_button.is_displayed())
            add_to_cart_button.click()
            
            # Example: navigate back
            back_button = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Back")
            back_button.click()
            
            home_screen_element = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Home Screen")
            self.assertTrue(home_screen_element.is_displayed(), "Did not navigate back to Home Screen.")

        except Exception as e:
            print(f"Test failed: {e}")
            self.driver.get_screenshot_as_file("deep_link_failure

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