How to Test Deep Links in Android: Three Different Options

Testing deep links in Android takes some effort, but you have all of the tools you need with ADB and Espresso. Here's how to get started.

Deep links are an important feature in mobile applications. By taking users directly to specific sections or pieces of content, they increase engagement. But, they're not easy to implement and require careful testing. If you don't make verifying deep links a part of your testing process, you risk leaving users disappointed and angry instead of engaged. 

Let's look at three different options for testing deep links. 

Testing Deep Links on Android

To follow this tutorial, you'll need to install Android Studio. This tutorial works with Kotlin version 1.15.x or newer and Android SDK version 29 or newer. 

We'll use a simple Kotlin Android application with two types of deep links; application links and web links. We'll very briefly look at how to implement a set of deep links and then dive right into how to test them. To focus on testing techniques, both the overall application and the links themselves are very simple. The application is based on the "Basic Activity" sample application in Android Studio. 

Android applications define their links in AndroidManifest.xml. Let's start with an overview of the two types of links and how to define them in the application manifest. 

Application Links

Application links use a proprietary scheme instead of the standard HTTP or HTTPS. You'll encounter them most often encountered on the device since we use them to move users between apps or application sections. 

Here's a definition of an application link that uses dleg for the scheme. 

 
<intent-filter>
   <action android:name="android.intent.action.VIEW" />
   <category android:name="android.intent.category.DEFAULT" />
   <category android:name="android.intent.category.BROWSABLE" />
   <data android:host="www.example.com" />
   <data android:scheme="dleg" />
</intent-filter>
 

So, Android will route links targeted to dleg://www.example.com to this application. 

Web Links

Web links use http and https. 

 
<intent-filter android:autoVerify="true">
   <action android:name="android.intent.action.VIEW" />
   <category android:name="android.intent.category.DEFAULT" />
   <category android:name="android.intent.category.BROWSABLE" />
   <data android:host="www.example.com" />
   <data android:scheme="http" />
   <data android:scheme="https" />
</intent-filter>
 

This definition looks largely the same as the previous one, except it has two scheme entries and the autoVerify="true" parameters. 

If you want Android to automatically route links for your domain to your application instead of the website, you need to verify the links using one of the methods spelled out in the documentation. If you don't, Android will prompt users with an option to open the links in your app or in a web browser. Take care of this in advance, or automated tests will fail when the operating system tries to prompt a use that isn't there. 

Application Code

The application handles deep links in MainActivity's onCreate method. This method passes the Intent to handleIntent, which verifies that it isn't carrying the default MAIN action. If it doesn't, the method passes the intent to ProcessDeepLinkIntent

This method checks to see if the op parameter is present. If it's present, it starts a new activity. If not, it checks for the text parameter and displays its value in the main view. 

  
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    setSupportActionBar(binding.toolbar)

    val navController = findNavController(R.id.nav_host_fragment_content_main)
    appBarConfiguration = AppBarConfiguration(navController.graph)
    setupActionBarWithNavController(navController, appBarConfiguration)

    binding.fab.setOnClickListener { view ->
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show()
    }
    handleIntent(intent)
}

private fun handleIntent(intent: Intent?) {
    val appLinkAction: String? = intent?.action
    if (appLinkAction.equals("android.intent.action.MAIN").not()) {
        val appLinkData: Uri? = intent?.data
        if (appLinkData != null) {
            processDeepLink(appLinkData)
        }
    }
}
    
private fun processDeepLink(appLinkData: Uri?) {
    val opvalue = appLinkData?.getQueryParameter("op")
    val textValue = appLinkData?.getQueryParameter("text")
        
    if (opvalue.isNullOrBlank().not()) {
        startActivity(Intent(this, NewActivity::class.java))
    } else if (textValue.isNullOrBlank().not()) {
        val textView: TextView = findViewById<TextView>(R.id.textview_first)
        textView.text = textValue
    }
}

 

So, we have a simple application that extracts and uses values from a deep link. A more sophisticated app might use the values of the op parameter to perform more than one action. Or, it might use a parameter to display an item for sale or send a user to a specific screen to read a message. 

Let's look a how we can test these links. 

Testing Deep Links With ADB

The Android Debug Bridge (ADB) makes it easy to perform ad hoc tests against deep links in the emulator or an attached Android device. With it, you call the activity manager to broadcast an intent. 

The syntax for sending an intent requires eight arguments: 

  • -s <device id> to select the correct device
  • shell - to send a shell command
  • am - to call the activity manager in the shell
  • start to start an activity
  • -W to wait for the activity to finish before returning
  • -a android.intent.action.VIEW to indicate a link
  • -d <link text>
  • <application package>

Here's a shell listing the available devices and then sending an application deep link to the connected emulator. Note that the URI escapes the spaces with a \ to avoid the shell trying to interpolate them as individual arguments. 

  
egoebelbecker@zaku:~/android-studio/bin$ adb devices
List of devices attached
emulator-5554	offline
emulator-5556	device

egoebelbecker@zaku:~/android-studio/bin$ adb -s emulator-5556 shell am start -W -a android.intent.action.VIEW -d "dleg://www.example.com?text=Testing\ application\ deep\ link" com.ericgoebelbecker.deeplinkskotlin
Starting: Intent { act=android.intent.action.VIEW dat=dleg://www.example.com?text=Testing application deep link pkg=com.ericgoebelbecker.deeplinkskotlin }
Status: ok
LaunchState: UNKNOWN (-1)
Activity: com.ericgoebelbecker.deeplinkskotlin/.MainActivity
WaitTime: 155
Complete
 

Here's the result in the emulator: 

The application received the deep link and displayed the text for us! 

Now, let's try a web link. 

 
egoebelbecker@zaku:~/android-studio/bin$ adb -s emulator-5556 shell am start -W -a android.intent.action.VIEW -d "https://www.example.com?text=Testing\ web\ deep\ link" com.ericgoebelbecker.deeplinkskotlin
Starting: Intent { act=android.intent.action.VIEW dat=https://www.example.com/... pkg=com.ericgoebelbecker.deeplinkskotlin }
Status: ok
LaunchState: UNKNOWN (-1)
Activity: com.ericgoebelbecker.deeplinkskotlin/.MainActivity
WaitTime: 211
Complete
 

The emulator displays the link text.

ADB and the activity manager don't differentiate between application and web links, so we only need to change the link we send with the intent. 

They're handy tools for verifying deep links as you're coding since you can use the shell to send links with varying parameters and make sure your code works as expected. They're also the only test tool we'll cover here that verifies the contents of your manifest file since they test your entire application instead of individual units. 

But they're not well suited for automated tests since you still need to look at the emulator to check your results. 

Testing Deep Links With Espresso Intents

Espresso is Google's toolkit for testing Android UIs, and Espresso-Intents is an extension for verifying and stubbing Android intents. We can use this extension to ensure that our application executes the intents we expect when we send it a link. 

Espresso-intents make these checks easy with the intended() method, which verifies that it saw an Intent that matches the given criteria during the test. With some simple setup, we can use it to check that the application sees the links we pass in. 

When the application is sent a link with the op parameter, it initiates a new activity by creating another Intent. Here is a test of that Android application deep link:

 
@RunWith(AndroidJUnit4::class)
class AppDeepLinkingIntents {

    @Rule
    @JvmField
    val activityTestRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun deepLinkWithOpGeneratesNewActivity() {

        Intents.init()

        val intentFoo = Intent(Intent.ACTION_VIEW, Uri.parse("dleg://www.example.com?op=doit"))
        activityTestRule.launchActivity(intentFoo)
        intended(
            allOf(
                hasComponent(NewActivity::class.java.name)
            )
        )
        Intents.release()
    }
}
 

First, we need an ActivityTestRule to run the activity, so we create it on line #6. 

Then, in the test method, we initialize Espression-Intents with Intents.init() on line #11, so it will capture the intent. Next, we create the intent on line #13 and pass it to ActivityTestRule.launchActivity(). This executes the test. 

Finally, we use intended() on line #15 with a matcher to verify that an Intent with a NewActivity component was seen during the test. 

Run this test in Android Studio: 

It's all green! 

Testing Deep Links With ActivityScenarioRule

We still need an automated test verifying that deep links result in the UI changes we expect. For this, we can use ActivityScenarioRule and Kotlin's ability to easily access views inside a unit test. 

We used ActivityTestRule in the previous test because it plays nicely with Espresso-Intents, but Google deprecated this class in an earlier version of Espresso. We're better off using ActivityScenarioRule when we can. It has a more concise syntax and, as you'll see, makes tests easy to read and write. 

Here's a test for the Android application deep link: 

 
@RunWith(AndroidJUnit4::class)
class AppDeepLinkingTest {

    private val intentFoo = Intent(Intent.ACTION_VIEW, Uri.parse("dleg://www.example.com?text=foo"))

    @get:Rule
    val fooRule = activityScenarioRule<MainActivity>(intentFoo)
    
    @Test
    fun deepLinkApplicationTextFooHasTextFoo() {
        val fooScenario = fooRule.scenario
        fooScenario.onActivity { activity ->
            val viewModel = activity.findViewById<TextView>(R.id.textview_first)
            Assert.assertEquals(viewModel.text, "foo")
        }
    }
}
 

On line #4, we create an Intent with the link and action required for the test. 

On line #7, we create the ActivityScenarioRule and initialize it with the Intent. 

Lines #11 and #12 get the ActivityScenario from the rule and execute it inside a closure. Inside the closure, we use the activity to find the TextView that should contain the text supplied by the deep link. So, we can verify that the link worked with an assertion. 

Run this test in Android Studio and check the result: 

Like with the ADB tests, the only difference for the web links is the contents of the Intent. 

  
@RunWith(AndroidJUnit4::class)
class HttpDeepLinkingTest {

    private val intentBar = Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com?text=bar"))

    @get:Rule
    val barRule = activityScenarioRule<MainActivity>(intentBar)

    @Test
    fun deepLinkWebTextBarHasTextBar() {

        val barScenario = barRule.scenario
        barScenario.onActivity { activity ->
            val viewModel = activity.findViewById<TextView>(R.id.textview_first)
            Assert.assertEquals(viewModel.text, "bar")
        }
    }
}
 

If you've taken care of the link verification, your tests will pass for web links, too. 

Verify Your Android Deep Links

This tutorial looked at three ways to test deep links in Android. ADB makes it easy for developers to verify links as they code. Espresso has tools for ensuring the Android UI responds correctly when it's sent a link, and Espresso-Intents is useful for stubbing and verifying intents inside tests. 

Android has a robust development ecosystem, with a cross-platform IDE in Android Studio and a versatile testing library in Espresso. They give you everything you need to set test your deep links. 

This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!).

Related articles