Espresso and Asynchronous jobs

Photo by tabitha turner on Unsplash

Espresso provides very limited synchronization capabilities. It waits for operations that post messages on the MessageQueue, such as a subclass of View that's drawing its contents on the screen. Because Espresso isn’t aware of any other asynchronous operations, including those running on a background thread, it will not be providing synchronization guarantees in those situations. These include operations like fetching data from a server ( unless its done using async task ), database operations etc.

In Espresso tests we use perform() and check(). Both these methods in turn ask Espresso to wait until the app is in idle state before performing the operation. But the definition of idle state does not include those actions that’s happening in the background. Hence before the app has got a value from network calls or database, the validation is done and the test fails.

Espresso has given solution a for this which is using IdlingResources. Not going to the details of that framework as the intention of this article is to explain my simple solution. This blog https://medium.com/azimolabs/wait-for-it-idlingresource-and-conditionwatcher-602055f32356#.ja9nytoe9 clearly explains how to add, use, the inconveniences and issues of IdlingResources.The main thing which I personally didn't like is that fact that you have to modify the source code in order to use IdlingResources.

Thus we were looking for solutions to get rid of all the Thread.sleep() that we had added in our Espresso tests to forcefully make test to wait for long running asynchronous tasks to finish.

The major contributor for asynchronous operations in our app is the api calls to fetch data from network. So the first choice was to use https://github.com/JakeWharton/okhttp-idling-resource. Unfortunately that did not work for us. I do not think it is issue with that library. I am yet to completely find out why the synchronization was not happening. One possible reason could be that we are using various okHttpClients for making various calls and few are done even from a react native code. So we might have missed to register some clients or not done it the right way. Another reason could be that this handles only the synchronization of network calls. Along with fetching data from network, our app also does heavy database operation of saving those. So it could be that the Espresso is synchronized with the network calls but not with other background thread operations. After trying to find the reason for two days, only answer I got was that its not the way to go for our app. Then I came across this library called Condition Watcher ( the blog link given above). Initial tests showed that it does the job but at the same time some stuff in the article was out dated. Also defining instructions for each condition to be checked seemed like lots of boiler plate code.

Thus i continued my search and trails and came up with these two methods.

inline fun <reified T : Activity> isVisible() : Boolean {
val am = InstrumentationRegistry.getInstrumentation().targetContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager
val visibleActivityName = am.appTasks[0].taskInfo.topActivity?.className
return visibleActivityName == T::class.java.name
}
val TIMEOUT = 8000L
val CONDITION_CHECK_INTERVAL = 100L

inline fun <reified T : Activity> waitUntilActivityVisible() {
val startTime = System.currentTimeMillis()
while (!isVisible<T>()) {
Thread.sleep(CONDITION_CHECK_INTERVAL)
if (System.currentTimeMillis() - startTime >= TIMEOUT) {
throw AssertionError("Activity ${T::class.java.simpleName} not visible after $TIMEOUT milliseconds")
}
}
}

Reference: http://www.douevencode.com/articles/2019-02/espresso-wait-for-activity-visible/

So using this method, you can make test to wait until an activity was visible. But in our case this was not sufficient. For eg, after landing on the HomePage in our app, some views will appear only after some other background operation is finished. Thus I had to make test wait till a particular view is visible.

For that I used this method

class WaitUntilVisibleAction(private val timeout: Long) : ViewAction {

override fun getConstraints(): Matcher<View> {
return any(View::class.java)
}

override fun getDescription(): String {
return "wait up to $timeout milliseconds for the view to become visible"
}

override fun perform(uiController: UiController, view: View) {

val endTime = System.currentTimeMillis() + timeout

do {
if (view.visibility == View.VISIBLE) return
uiController.loopMainThreadForAtLeast(50)
} while (System.currentTimeMillis() < endTime)

throw PerformException.Builder()
.withActionDescription(description)
.withCause(TimeoutException("Waited $timeout milliseconds"))
.withViewDescription(HumanReadables.describe(view))
.build()
}
}

/**
* @return a [WaitUntilVisibleAction] instance created with the given [timeout] parameter.
*/
fun waitUntilVisible(timeout: Long): ViewAction {
return WaitUntilVisibleAction(timeout)
}

Reference: https://adilatwork.blogspot.com/2020/08/espresso-tests-wait-until-view-is.html

With these two I could get rid of all the Thread.sleep and make Espresso wait for asynchronous operations to complete.

Below is usage from our test case.

onView(withId(R.id.passCodeTextField)).perform(typeText("testPassCode"),
closeSoftKeyboard())
onView(withId(R.id.signInButton)).perform(click())
// Thread.sleep(3000)
onView(withId(R.id.userName)).perform(waitUntilVisible(5000L))
onView(withId(R.id.userName)).check(matches(withText("${mockvariables.Firstname} ${mockvariables.Lastname}")))
onView(withId(R.id.okButton)).perform(click())
// Thread.sleep(10000)
waitUntilActivityVisible
<HomeActivity>()
onView(withId(R.id.collectedCount)).perform(waitUntilVisible(5000L))
onView(withId(R.id.eventLabel)).check(matches(withText(mockvariables.eventName)))

--

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

CSRF Synchronizer Token Pattern and Double Submit Cookies Pattern in PHP

Install Ingress-Nginx and ExternalDNS with Pulumi on GKE Autopilot

My Open Source Development Journey

Ultimate Beginner Guide to Streamlabs OBS (OBS)

Day 1: Why 100 days of A11y?

Staircase leading to top of hill

Two-Tier WordPress architecture based on Containers

Building a Kotlin Mobile App With the Salesforce SDK

Python basics

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Preetha Ng

Preetha Ng

More from Medium

How to Hire Android Developers

Ace of Spades — Asymmetrical Play Patterns

Coding a Better World Together with Uncle Bob

Why 0.04–0.03 is not equal to 0.01 (sometimes!!!)?