Introduction ##
When working with Espresso tests, you might have found it hard to make Espresso wait for background tasks to complete before performing other actions or assertions. Fortunately, the classes in the Espresso idling package exist to cover this use case.
In this tutorial, we will learn how to use one of those classes called CountingIdlingResource.
Goals
At the end of the tutorial, you would have learned:
- How to use CountingIdlingResource 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.
- Basic understanding of async operations.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Add the dependencies below into your module build.gradle file.
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0' androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0'
-
Replace activity_main.xml with the code below. We simply changed the
textSize
and added anandroid:id
to TextView.<?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"> <TextView android:id="@+id/textView_helloWorld" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="32sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
Replace the entire class ExampleInstrumentedTest with the code below. Besides the commented out lines, it is just a simple Espresso test that performs a click on the TextView, and then verifies that the new text content is correct. You do not have to understand the commented out parts for now.
@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { //Gets the IdlingRegistry singleton //private val idlingResourceRegistry = IdlingRegistry.getInstance() @get:Rule val activityRule = ActivityScenarioRule(MainActivity::class.java) @Test fun doAsyncTest(){ //Register the CountingIdlingResource before click() //idlingResourceRegistry.register(countingIdlingResource) onView(withId(R.id.textView_helloWorld)) .perform(click()) .check(matches(withText("WorldHello!"))) } /* @After fun unregisterIdlingResources(){ //Unregisters the CountingIdlingResource idlingResourceRegistry.resources.forEach { idlingResourceRegistry.unregister(it) } }*/ }
-
Create a new class called HttpClient using the code below. This class contains a single function that will suspend for three seconds, and then assign a new text value to a TextView object.
class HttpClient { fun doLongAsync(textView: TextView) { //Incrementing counter when work starts //countingIdlingResource.increment() CoroutineScope(Dispatchers.Main).launch { delay(3000) textView.text = "WorldHello!" //countingIdlingResource.decrement() } } }
-
In the same file, add the singleton below. You do not need to understand it for now.
/* We can use a global singleton for this project because We don't have more than one test using this IdlingResourceCounter It is recommended to add IdlingResourceCounter directly into your Production code */ object IdlingResourceCounter { private const val IDLING_RESOURCE_NAME = "GlobalIdlingResourceCounter" val countingIdlingResource = CountingIdlingResource(IDLING_RESOURCE_NAME) }
-
Finally, append the code below to
MainActivity#onCreate()
.val textView = findViewById<TextView>(R.id.textView_helloWorld) val httpClient = HttpClient() textView.setOnClickListener { //Don't pass a View to a Service in a real app! httpClient.doLongAsync(it as TextView) }
Project Overview
The tutorial app contains a single TextView, after clicking on it and waits for 3 seconds, then its value will change to WorldHello!
from Hello World!
.
Now let us look at the instrument test doAsyncTest()
in ExampleInstrumentedTest.
doAsyncTest()
will fail because Espresso is not aware of the background task.
androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'an instance of android.widget.TextView and view.getText() with or without transformation to match: is "WorldHello!"' doesn't match the selected view.
Expected: an instance of android.widget.TextView and view.getText() with or without transformation to match: is "WorldHello!"
It tries to check for the WorldHello!
value immediately after clicking on the TextView. There are only three conditions where Espresso will wait:
- The MessageQueue is empty.
- There is no running AsyncTask.
- There is no idling resource. CountingIdlingResource is one of such resources that we are learning about in this tutorial.
Creating the CountingIdlingResource object
The object IdlingResourceCounter contains a public field called countingIdlingResource, which is an instance of CountingIdlingResource. The only argument that the CountingIdlingResource constructor requires is a String value that explains what the instance of CountingIdlingResource is used for.
object IdlingResourceCounter {
private const val IDLING_RESOURCE_NAME = "GlobalIdlingResourceCounter"
val countingIdlingResource = CountingIdlingResource(IDLING_RESOURCE_NAME)
}
We are only able to use a global object in this tutorial because we only have one test. It is recommended that you provide a CountingIdlingResource for each instance of your async-worker class.
CountingIdlingResource and Service class
Your Service or Repository classes should increment()
the counter when starting long-running async work and decrement()
the counter when the work is complete. To achieve this, uncomment the respective lines in the HttpClient class, so it should look like the code below.
class HttpClient {
fun doLongAsync(textView: TextView) {
//Incrementing counter when work starts
countingIdlingResource.increment()
CoroutineScope(Dispatchers.Main).launch {
delay(3000)
textView.text = "WorldHello!"
countingIdlingResource.decrement()
}
}
}
Register CountingIdlingResource to IdlingResourceRegistry
Simply having a working CountingIdlingResource is not enough. We still have to register the CountingIdlingResource instance to the IdlingResourceRegistry. IdlingResourceRegistry is a singleton. We already have the code to retrieve it in ExampleInstrumentedTest, so retrieve it by uncomment the line of code below.
private val idlingResourceRegistry = IdlingRegistry.getInstance()
Now that we have the IdlingResourceRegistry, we can register CountingIdlingResource to it by uncommenting this line of code inside doAsyncTest()
.
idlingResourceRegistry.register(countingIdlingResource)
We also have to unregister the idling resource after the test is done, so uncomment the function unregisterIdlingResources()
as well.
@After
fun unregisterIdlingResources(){
//Unregisters the CountingIdlingResource
idlingResourceRegistry.resources.forEach {
idlingResourceRegistry.unregister(it)
}
}
We should have everything we need for a working test. If we run the test now, we can see that Espresso will wait for 3 seconds before performing the check()
.
Solution Code
ExampleInstrumentedTest.kt
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
//Gets the IdlingRegistry singleton
private val idlingResourceRegistry = IdlingRegistry.getInstance()
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun doAsyncTest(){
//Register the CountingIdlingResource before click()
idlingResourceRegistry.register(countingIdlingResource)
onView(withId(R.id.textView_helloWorld))
.perform(click())
.check(matches(withText("WorldHello!")))
}
@After
fun unregisterIdlingResources(){
//Unregisters the CountingIdlingResource
idlingResourceRegistry.resources.forEach {
idlingResourceRegistry.unregister(it)
}
}
}
HttpClient.kt
class HttpClient {
fun doLongAsync(textView: TextView) {
//Incrementing counter when work starts
countingIdlingResource.increment()
CoroutineScope(Dispatchers.Main).launch {
delay(3000)
textView.text = "WorldHello!"
countingIdlingResource.decrement()
}
}
}
/*
We can use a global singleton for this project because
We don't have more than one test using this IdlingResourceCounter
It is recommended to add IdlingResourceCounter directly into your
Production code
*/
object IdlingResourceCounter {
private const val IDLING_RESOURCE_NAME = "GlobalIdlingResourceCounter"
val countingIdlingResource = CountingIdlingResource(IDLING_RESOURCE_NAME)
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.textView_helloWorld)
val httpClient = HttpClient()
textView.setOnClickListener {
//Don't pass a View to a Service in a real app!
httpClient.doLongAsync(it as TextView)
}
}
}
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">
<TextView
android:id="@+id/textView_helloWorld"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Module build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.example.daniwebandroidcountingidlingresource"
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'
implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
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'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
}
Summary
We have learned how to use CountingIdlingResource to make Espresso wait in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidCountingIdlingResource.