Android Native - How to validate Intents in Espresso tests

dimitrilc 1 Tallied Votes 65 Views Share

Introduction ##

In this tutorial, we will learn how to filter and validate Intents fired from the application under test.

Goals

At the end of the tutorial, you would have learned:

  1. How to filter and validate Intents in Espresso tests.

Tools Required

  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1 Patch 1.

Prerequisite Knowledge

  1. Intermediate Android.
  2. Basic Espresso.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the <string> resource below into strings.xml.

     <string name="launch_intent">Launch Intent</string>
  3. Replace the code inside activity_main.xml with the code below. This simply adds a Button.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button_launchIntent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/launch_intent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Add the dependencies below into your module build.gradle file.

     androidTestImplementation 'androidx.test:runner:1.4.0'
     androidTestImplementation 'androidx.test:rules:1.4.0'
     androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
  5. Remove all test cases from ExampleInstrumentedTest in your androidTest source set. Your ExampleInstrumentedTest file should look like the empty class below.

     @RunWith(AndroidJUnit4::class)
     class ExampleInstrumentedTest {
     }
  6. Append MainActivity#onCreate() with the code below.

     findViewById<Button>(R.id.button_launchIntent).setOnClickListener {
        val intent = Intent(ACTION_VIEW).apply {
            data = intentData
        }
    
        startActivity(intent)
     }
  7. Add the companion object below into MainActivity as well.

     companion object {
        val intentData: Uri = Uri.Builder()
            .scheme("geo")
            .query("0,0")
            .appendQueryParameter("q", "First St SE, Washington, DC, 20004")
            .build()
     }

Project Overview

The tutorial app is a super simple app with a single Button. After clicking on the Button, the app will attempt to launch an Activity that can consume the Uri scheme geo. There is only one app on my emulator that can do this, which is the default Google Maps app.

The Intent launched is an implicit Intent with data pointing to a specific location (US Capitol) on the world map. The data used for the Intent is shown below.

val intentData: Uri = Uri.Builder()
   .scheme("geo")
   .query("0,0")
   .appendQueryParameter("q", "First St SE, Washington, DC, 20004")
   .build()

Reference the animation below to get a feel of what the application does.

Map_App.gif

Basic Espresso Test

Copy and paste the code below into your ExampleInstrumentedTest class.

   @get:Rule
   val activityRule = ActivityScenarioRule(MainActivity::class.java)

/*    @Before
   fun startCapturingIntent(){
       Intents.init()
   }*/

/*    @After
   fun clearIntentsState() {
       Intents.release()
   }*/

   @Test
   fun intentTest(){
       onView(withId(R.id.button_launchIntent))
           .perform(click())

/*        Intents.intended(allOf(
           hasAction(ACTION_VIEW),
           hasData(MainActivity.intentData)
       ))*/
   }

The test intentTest() simply opens the app and then performs a click().

Map_Test_No_Capture.gif

The test is still not aware of the Intent being fired yet, but the commented out sections of the code can help us capture and verify the Intent being fired.

The Intents class

For us to be able to capture Intents from the application under test, we can call Intents.init() before each test. Go ahead and uncomment startCapturingIntent().

@Before
fun startCapturingIntent(){
   Intents.init()
}

Because Intents#init() will modify an internal cache of Intents, it is very important that we must call Intents#release() after each test is completed to clear out this cache. Go ahead and uncomment the function clearIntentsState().

@After
fun clearIntentsState() {
   Intents.release()
}

Accessing the captured Intents

The Intents class provides a couple of methods to access the captured Intents.

  1. getIntents(): retrieve all captured Intents in a list.
  2. intented() variants: find one or more Intents and perform verifications on them.
  3. intending(): used for stubbing Intent responses.

In intentTest(), the method chosen was intended(). Go ahead and uncomment it out.

Intents.intended(allOf(
   hasAction(ACTION_VIEW),
   hasData(MainActivity.intentData)
))

To verify Intent information, we can use the convenient methods from the IntentMatchers class. hasAction() and hasData() do not exist on the vanilla Matchers class.

You can run the test now and verify whether it passes.

Map_Test_With_Capture.gif

Solution Code

ExampleInstrumentedTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

   @get:Rule
   val activityRule = ActivityScenarioRule(MainActivity::class.java)

   @Before
   fun startCapturingIntent(){
       Intents.init()
   }

   @After
   fun clearIntentsState() {
       Intents.release()
   }

   @Test
   fun intentTest(){
       onView(withId(R.id.button_launchIntent))
           .perform(click())

       Intents.intended(allOf(
           hasAction(ACTION_VIEW),
           hasData(MainActivity.intentData)
       ))
   }

}

MainActivity.kt

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       findViewById<Button>(R.id.button_launchIntent).setOnClickListener {
           val intent = Intent(ACTION_VIEW).apply {
               data = intentData
           }

           startActivity(intent)
       }
   }

   companion object {
       val intentData: Uri = Uri.Builder()
           .scheme("geo")
           .query("0,0")
           .appendQueryParameter("q", "First St SE, Washington, DC, 20004")
           .build()
   }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button_launchIntent"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/launch_intent"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Validate Intents</string>
   <string name="launch_intent">Launch Intent</string>
</resources>

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidvalidateintents"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.6.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

   androidTestImplementation 'androidx.test:runner:1.4.0'
   androidTestImplementation 'androidx.test:rules:1.4.0'
   androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
}

Summary

We have learned how to test Intents in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidValidateIntents.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.