How to Test Deep Links on Flutter (Complete Guide)

Testing deep links on Flutter applications is a critical aspect of ensuring a seamless user experience and robust application navigation. A deep link, fundamentally, is a URL that directs users to spe

June 22, 2026 · 15 min read · How-To Guides

Testing deep links on Flutter applications is a critical aspect of ensuring a seamless user experience and robust application navigation. A deep link, fundamentally, is a URL that directs users to specific content or screens within your app, bypassing the app's usual navigation flow. This guide provides a comprehensive overview of how to test deep links on Flutter, covering everything from understanding their importance and potential pitfalls to implementing manual and automated testing strategies with practical examples. Without thorough deep link testing, users might encounter broken navigation, incorrect content display, or even app crashes when interacting with marketing campaigns, social shares, or system notifications, leading to significant user frustration and abandonment.

Deep links are essential for improving user engagement and retention. They facilitate personalized onboarding, enable sharing of specific in-app content, and power features like "add to cart" links from promotional emails or password reset flows directly into the app. However, their implementation often involves complex interactions between the operating system, the app's manifest/Info.plist, Flutter's navigation stack, and potentially third-party services like Firebase Dynamic Links or Branch.io. This complexity introduces numerous points of failure, making dedicated testing indispensable. This article will equip QA engineers and developers with the knowledge and tools to confidently validate deep link functionality across various scenarios in their Flutter applications.

Understanding Flutter Deep Links and Their Importance

Deep links in Flutter applications leverage platform-specific mechanisms to direct users to content. On Android, this typically involves intent filters defined in AndroidManifest.xml, while on iOS, it uses Universal Links (associated domains) or Custom URL Schemes configured in Info.plist and the Associated Domains Entitlement. Flutter's go_router or Navigator 2.0 API then interprets these incoming URLs to navigate to the correct widget tree.

Why Deep Links Break in Production

Despite careful implementation, deep links are prone to breaking for several reasons:

Core Deep Link Concepts in Flutter

Designing a Comprehensive Deep Link Test Matrix

A structured test matrix is crucial for covering all deep link scenarios. We need to consider happy paths, error conditions, edge cases, and even accessibility and security implications.

Deep Link Test Matrix Example

Here's a template for a deep link test matrix. This should be adapted and expanded based on your specific application's deep link patterns and features.

Test Case IDDeep Link URL / ScenarioExpected Outcome (App Behavior)Initial App StatePreconditions / SetupTest StepsPass/Fail CriteriaNotes
DL-001myapp://product/123App opens to Product Details screen for ID 123App ClosedApp installed1. Tap linkProduct ID 123 displayedHappy Path
DL-002https://myapp.com/profileApp opens to User Profile screenApp BackgroundUser logged in1. Tap linkProfile screen content displayedUniversal Link
DL-003myapp://settings?tab=notificationsApp opens to Settings, Notifications tab activeApp Open (Home)1. Tap linkSettings screen, Notifications tab selectedQuery Params
DL-004myapp://invalid/pathApp opens to Home screen or 404/Error screenApp Closed1. Tap linkAppropriate error handlingError Path
DL-005myapp://product/99999 (non-existent ID)App opens to Product List or error messageApp Closed1. Tap link"Product not found" or similarEdge Case
DL-006myapp://auth/reset?token=XYZApp opens to Reset Password screen, token pre-filledApp ClosedUser logged out1. Tap linkReset Password form with tokenSecurity
DL-007myapp://product/123App opens to Product Details screen for ID 123App ClosedNo internet connection1. Tap linkProduct details with offline indicatorOffline
DL-008myapp://product/123App opens, then back button navigates to previous screen (if applicable)App Open (Home)1. Tap link. 2. Tap back buttonCorrect navigation stack behaviorNavigation
DL-009myapp://product/123Screen reader announces "Product Details for item 123"App ClosedTalkBack/VoiceOver enabled1. Tap linkAccessibility announcementAccessibility
DL-010myapp://product/123App opens, user is prompted to log in if not alreadyApp ClosedUser not logged in1. Tap linkLogin screen, then redirects to product after loginAuthentication

Persona-Driven Deep Link Testing

Different user personas might interact with deep links differently, or their experience might be impacted in unique ways. For example:

Tools like SUSATest, with its autonomous, persona-driven exploration capabilities, can be invaluable here. Instead of just asserting a specific screen loads, SUSATest can launch an app via a deep link, then have an "Impatient User" persona immediately try to interact or a "Curious User" persona explore related content. This reveals issues that a simple "screen loads" assertion would miss, such as a deep link leading to a screen with slow loading times or broken navigation within the deep-linked context.

Manual Deep Link Testing Approaches

Manual testing is foundational for deep links, especially during initial development and for ad-hoc checks. It allows for quick feedback and observation of the user experience.

Step-by-Step Manual Testing Guide

  1. Preparation:
  1. Testing Custom URL Schemes (e.g., myapp://):

Replace com.example.my_app with your app's package name. The -W flag waits for the launch to complete, and -a android.intent.action.VIEW is the standard action for opening URLs.

Find using xcrun simctl list devices.

Open this HTML file in the device's browser and tap the link.

  1. Testing App Links (Android) / Universal Links (iOS) (e.g., https://myapp.com/):
  1. Verifying App Behavior:
  1. Logging and Debugging:

// Example of logging incoming deep links using uni_links
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _initUniLinks();
  }

  Future<void> _initUniLinks() async {
    // Handle initial deep link when app is launched
    try {
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        print('Initial Deep Link: $initialLink');
        _handleDeepLink(Uri.parse(initialLink));
      }
    } catch (e) {
      print('Failed to get initial deep link: $e');
    }

    // Handle deep links when app is already running
    getLinkStream().listen((String? link) {
      if (link != null) {
        print('Streamed Deep Link: $link');
        _handleDeepLink(Uri.parse(link));
      }
    }, onError: (err) {
      print('Error handling deep link stream: $err');
    });
  }

  void _handleDeepLink(Uri uri) {
    // Implement your routing logic here based on the URI
    print('Handling URI: ${uri.path}, Query: ${uri.queryParameters}');
    if (uri.pathSegments.contains('product') && uri.pathSegments.length > 1) {
      final productId = uri.pathSegments.last;
      // Navigate to ProductDetailsScreen(productId: productId)
      print('Navigating to product details for ID: $productId');
    } else if (uri.path == '/profile') {
      // Navigate to ProfileScreen()
      print('Navigating to profile screen');
    } else {
      // Default navigation or error screen
      print('Unknown deep link: $uri');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Deep Link Test App')),
        body: Center(child: Text('Waiting for deep link...')),
      ),
    );
  }
}

Automated Deep Link Testing Approaches

Automating deep link tests is crucial for regression testing and ensuring deep link functionality remains stable across releases. This is where Flutter's testing ecosystem shines.

Unit/Widget Testing for Deep Link Parsing Logic

Before even launching the app, you can unit test your URI parsing and routing logic. This verifies that your go_router configuration or custom parsing functions correctly interpret deep link URLs.


// Example: Unit testing go_router deep link parsing
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart'; // Required for MaterialPage and GoRoute

// Define your GoRouter configuration
final GoRouter _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => Text('Home Screen'),
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        final productId = state.pathParameters['id'];
        return Text('Product Details for ID: $productId');
      },
    ),
    GoRoute(
      path: '/profile',
      builder: (context, state) => Text('Profile Screen'),
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => Text('Settings Screen'),
      routes: [
        GoRoute(
          path: 'notifications', // /settings/notifications
          builder: (context, state) => Text('Notifications Settings'),
        ),
      ],
    ),
    GoRoute(
      path: '/error',
      builder: (context, state) => Text('Error Screen'),
    ),
  ],
  errorBuilder: (context, state) => Text('404 Not Found'),
);


void main() {
  group('Deep Link Routing with go_router', () {
    testWidgets('should navigate to product details with valid ID', (tester) async {
      final router = _router;
      await tester.pumpWidget(MaterialApp.router(routerConfig: router)); // Using routerConfig

      // Simulate deep link navigation
      router.go('/product/456');
      await tester.pumpAndSettle();

      expect(find.text('Product Details for ID: 456'), findsOneWidget);
    });

    testWidgets('should navigate to profile screen', (tester) async {
      final router = _router;
      await tester.pumpWidget(MaterialApp.router(routerConfig: router));

      router.go('/profile');
      await tester.pumpAndSettle();

      expect(find.text('Profile Screen'), findsOneWidget);
    });

    testWidgets('should navigate to nested settings/notifications', (tester) async {
      final router = _router;
      await tester.pumpWidget(MaterialApp.router(routerConfig: router));

      router.go('/settings/notifications');
      await tester.pumpAndSettle();

      expect(find.text('Notifications Settings'), findsOneWidget);
    });

    testWidgets('should show 404 for unknown path', (tester) async {
      final router = _router;
      await tester.pumpWidget(MaterialApp.router(routerConfig: router));

      router.go('/unknown-path');
      await tester.pumpAndSettle();

      expect(find.text('404 Not Found'), findsOneWidget);
    });

     testWidgets('should handle invalid product ID gracefully (example of internal logic)', (tester) async {
      // This test assumes internal logic would handle non-numeric IDs,
      // or if the builder for '/product/:id' had validation.
      // For go_router itself, it will still navigate to the widget
      // but the widget's logic would then handle the invalid ID.
      final router = GoRouter(
        initialLocation: '/',
        routes: [
          GoRoute(
            path: '/product/:id',
            builder: (context, state) {
              final productId = state.pathParameters['id'];
              if (int.tryParse(productId ?? '') == null) {
                return Text('Invalid Product ID: $productId');
              }
              return Text('Product Details for ID: $productId');
            },
          ),
          GoRoute(
            path: '/',
            builder: (context, state) => Text('Home'),
          ),
        ],
      );

      await tester.pumpWidget(MaterialApp.router(routerConfig: router));

      router.go('/product/abc'); // Invalid ID
      await tester.pumpAndSettle();

      expect(find.text('Invalid Product ID: abc'), findsOneWidget);
    });
  });
}

Integration/End-to-End Testing

Full integration tests are necessary to verify that the entire deep link flow, from OS intent to app navigation, works correctly.

#### Using flutter_driver (Legacy) or integration_test

For Flutter integration tests, integration_test (part of the Flutter SDK) is the recommended modern approach. It runs tests on a real device or emulator and interacts with the app as a user would.

Setting up integration_test:

  1. Add integration_test to dev_dependencies in pubspec.yaml:
  2. 
        dev_dependencies:
          flutter_test:
            sdk: flutter
          integration_test:
            sdk: flutter
    
  3. Create a test file (e.g., integration_test/app_test.dart).

Example: Deep Link Integration Test

The challenge with integration_test for deep links is directly simulating platform-level intents (like adb shell am start). You generally need a helper shell script or a specialized test framework to trigger the deep link *externally* and then have integration_test assert the outcome *internally*.

Here's an approach that combines a shell script to launch the deep link with integration_test to verify the state:

1. integration_test/deep_link_test.dart (Flutter side):


import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:your_app_name/main.dart' as app; // Replace with your app's main file

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Deep Link Integration Tests', () () {
    testWidgets('app opens to product details via deep link', (tester) async {
      app.main(); // Start your app
      await tester.pumpAndSettle();

      // At this point, the app should be open due to the external deep link.
      // We need to wait for the deep link to be processed.
      // This often requires a short delay or listening for a specific widget.

      // For demonstration, let's assume the product ID 123 is displayed
      // after a deep link like myapp://product/123
      expect(find.text('Product Details for ID: 123'), findsOneWidget);

      // Verify navigation stack (e.g., can go back to home)
      final NavigatorState navigator = tester.state(find.byType(Navigator));
      expect(navigator.canPop(), isTrue); // Should be able to pop if deep link pushed a route
      navigator.pop();
      await tester.pumpAndSettle();
      expect(find.text('Home Screen'), findsOneWidget); // Assuming Home is the initial screen
    });

    testWidgets('app opens to profile via deep link', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      expect(find.text('Profile Screen'), findsOneWidget);
    });

    // Add more tests for error cases, query parameters, etc.
  });
}

2. run_deep_link_test.sh (Shell script to orchestrate):

This script would launch the app with the deep link, then trigger the integration_test. This requires running the Flutter app in a special test mode or having a way for the test to signal it's ready for verification.


#!/bin/bash

# Configuration
PACKAGE_NAME="com.example.your_app_name" # Replace with your Android package name
IOS_BUNDLE_ID="com.example.yourappname" # Replace with your iOS bundle ID
DEEP_LINK_ANDROID="myapp://product/123"
DEEP_LINK_IOS="myapp://product/123"

# --- Android Test ---
echo "Running Android deep link test..."

# 1. Clear app data (optional, ensures fresh start)
adb shell pm clear $PACKAGE_NAME

# 2. Launch the app with the deep link
echo "Launching Android app with deep link: $DEEP_LINK_ANDROID"
adb shell am start -W -a android.intent.action.VIEW -d "$DEEP_LINK_ANDROID" $PACKAGE_NAME

# 3. Wait for the app to initialize (adjust delay as needed)
sleep 5

# 4. Run the integration test to assert the UI state
echo "Running Flutter integration test for Android..."
flutter test integration_test/deep_link_test.dart --target=integration_test/deep_link_test.dart -d emulator-5554 # Replace with your emulator ID

if [ $? -eq 0 ]; then
    echo "Android deep link test PASSED"
else
    echo "Android deep link test FAILED"
    exit 1
fi

echo ""

# --- iOS Test (Simulator) ---
echo "Running iOS deep link test..."

# Get simulator ID (take the first available iPhone simulator)
SIMULATOR_ID=$(xcrun simctl list devices | grep -m 1 "iPhone" | awk -F '[()]' '{print $2}')
if [ -z "$SIMULATOR_ID" ]; then
    echo "No iOS simulator found. Skipping iOS test."
else
    echo "Using iOS Simulator ID: $SIMULATOR_ID"

    # 1. Uninstall and reinstall app (optional, ensures fresh start)
    xcrun simctl uninstall $SIMULATOR_ID $IOS_BUNDLE_ID
    flutter build ios --simulator
    xcrun simctl install $SIMULATOR_ID build/ios/iphonesimulator/Runner.app

    # 2. Open the deep link
    echo "Launching iOS app with deep link: $DEEP_LINK_IOS"
    xcrun simctl openurl $SIMULATOR_ID "$DEEP_LINK_IOS"

    # 3. Wait for the app to initialize
    sleep 5

    # 4. Run the integration test
    echo "Running Flutter integration test for iOS..."
    flutter test integration_test/deep_link_test.dart --target=integration_test/deep_link_test.dart -d $SIMULATOR_ID

    if [ $? -eq 0 ]; then
        echo "iOS deep link test PASSED"
    else
        echo "iOS deep link test FAILED"
        exit 1
    fi
fi

echo "Deep link testing complete."

This combined approach is more robust but also more complex to manage.

Using Autonomous QA Platforms for Deep Link Testing

Autonomous QA platforms like SUSATest offer a fundamentally different and often more effective approach to deep link testing, especially for scenarios traditional scripted tests miss. Instead of writing explicit test steps, you provide the app and the deep link, and the platform intelligently explores.

How SUSATest Approaches Deep Link Testing:

  1. Direct Deep Link Injection: You can instruct SUSATest to launch your Flutter app using a specific deep link URL. This simulates the real-world scenario of a user tapping a link.
  1. Persona-Driven Exploration from Deep Link: Once the app is launched via the deep link, SUSATest's AI-driven personas take over.

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