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:
- How to filter and validate Intents in Espresso tests.
Tools Required
- Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1 Patch 1.
Prerequisite Knowledge
- Intermediate Android.
- Basic Espresso.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Add the
<string>
resource below into strings.xml.<string name="launch_intent">Launch Intent</string>
-
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>
-
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'
-
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 { }
-
Append
MainActivity#onCreate()
with the code below.findViewById<Button>(R.id.button_launchIntent).setOnClickListener { val intent = Intent(ACTION_VIEW).apply { data = intentData } startActivity(intent) }
-
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.
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()
.
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.
getIntents()
: retrieve all captured Intents in a list.intented()
variants: find one or more Intents and perform verifications on them.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.
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.