Avoiding sleep in Robolectric/Espresso test

Viewed 349

I am writing an end-to-end test using Robolectric which triggers an HTTP call to an OkHttp MockWebServer. It looks like this:

val mockServer = MockWebServer()
launchFragmentInHiltContainer<LoginFragment>()

onView(withId(R.id.loginEditText)).perform(typeText("loginId"))
onView(withId(R.id.loginButton)).perform(click())

// At this point, the fragment's viewmodel starts an HTTP request using Retrofit/OkHttp
// The callback causes a LiveData viewmodel to post an event, which the fragment listens
// to, and updates the UI accordingly

val request = mockServer.takeRequest()
assertEquals("/login", req.path)

Thread.sleep(1000)
shadowOf(getMainLooper()).idle()

// Without the two lines above, the test reaches this point before 
// the mock server is done calling back
onView(withId(R.id.loggedInView)).check(matches(isDisplayed()))

How can I make sure that the server response is fully processed after clicking the login button?

3 Answers

I am not 100% sure but try it once

Make an Object like this

 Object syncObject = new Object();

then replace your Thread.sleep(1000) with this

synchronized (syncObject) {
          syncObject.notify();
        }

if it is not working fine then put wait() in place of notify().

You can use Espresso idling resources (link: Documentation) in order to achieve this and get rid of that Thread.sleep() which is not a good option at all. Since you are using Okhttp there is already a library which should do the trick for you: Okhttp Idling Resource.

You don't need to inject wait statements to your code to detect the termination of a request. As you probably are well aware Thread.sleep(X) alone is a really bad attempt to solve this since the time your request takes changes every time and you risk either not waiting enough or waiting too much.

Idling Resources is a better attempt but suffers from multiple issues. 1)Between each check whether the queue is empty (there are no requests)it makes you wait 5 seconds. This piles up really quickly when you execute lots of tests. 2)You need to change the code you are testing to enable idling resources. 3)It doesn't work for all kinds of async requests.

I (and lots of other people) use a custom retry function in Espresso to solve this issue.

The function below checks whether a condition holds and if it doesn't, it checks it again after X milliseconds. With this, your tests will proceed as soon as the request is completed(instead of the 5 second wait time in IdlingResources). All you need to do is to wait for a condition that did not hold before your request was sent.

fun waitForCondition(matcher: Matcher<View>,
                     timeout: Long = 15000L,
                     condition: (View?) -> Boolean) {

    require(timeout > 0) { "Require timeout>0" }

    var success = false
    lateinit var exception: NoMatchingViewException
    val loopCounter = timeout / 250L
    (0..loopCounter).forEach {
        onView(matcher).check { view, noViewFoundException ->
            if (condition(view)) {
                success = true
                return@check
            } else {
                Thread.sleep(250)
                exception = noViewFoundException
            }
        }
        if (success) {
            return
        }
    }
    throw exception
}
Related