Making a CoroutineDispatcher IdlingResource for Espresso Tests

Viewed 1340

I'm trying to find a way to nicely implement an IdlingResource that will poll a CoroutineDispatcher's isActive property. However, from debugging, there never seems to be an active Job when checking this property.

So far I've tried using AsyncTask's THREAD_POOL_EXECUTOR for built-in idling, but it doesn't seem to work when using the asCoroutineDispatcher extension function and using that resulting CoroutineDispatcher to launch my ViewModel's job. I've attempted writing a custom IdlingResource

ViewModel

fun authenticate(username: String, password: String) = viewModelScope.launch(Dispatchers.Default) {
    if (_authenticateRequest.value == true) {
        return@launch
    }

    _authenticateRequest.postValue(true)
    val res = loginRepo.authenticate(username, password)
    _authenticateRequest.postValue(false)

    when {
        res is Result.Success -> {
            _authenticateSuccess.postValue(res.item)
        }
        res is Result.Failure && res.statusCode.isHttpClientError -> {

            _authenticateFailure.postValue(R.string.invalid_password)
        }
        else -> {
            _authenticateFailure.postValue(R.string.network_error)
        }
    }
}

IdlingResource

class CoroutineDispatcherIdlingResource(
    private val resourceName: String,
    private val dispatcher: CoroutineDispatcher
) : IdlingResource {
    private var callback: IdlingResource.ResourceCallback? = null

    override fun getName() = resourceName

    override fun isIdleNow(): Boolean {
        if (dispatcher.isActive) { return false }

        callback?.onTransitionToIdle()
        return true
    }

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
        this.callback = callback
    }
}

Espresso Test

@RunWith(AndroidJUnit4::class)
class LoginIntegrationTest {
    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    var idlingResource: CoroutineDispatcherIdlingResource? = null

    @Before
    fun before() {
        idlingResource = CoroutineDispatcherIdlingResource(this.javaClass.simpleName, Dispatchers.Default)
        IdlingRegistry.getInstance().register(idlingResource)
    }

    @Test
    fun loginFailure() {
        onView(withId(R.id.username))
            .perform(clearText()).perform(typeText("aslkdjqwe"))
        onView(withId(R.id.password))
            .perform(clearText()).perform(typeText("oxicjqwel"))
        onView(withId(R.id.login_button))
            .perform(click())

        onView(withId(com.google.android.material.R.id.snackbar_text))
            .check(matches(withText(R.string.invalid_password)))
    }
}

I'm expecting the isActive property to be true once the ViewModel 'authenticate' function is called, but this doesn't seem to be the case. It always appears to be false, since there's never an active Job in the CoroutineDispatcher.

2 Answers

Figured out a solution! It turns out that AsyncTask's THREAD_POOL_EXECUTOR was actually working fine for this. What I was missing was having an IdlingResource for Retrofit/OkHttp.

My initial assumption was that the coroutine running on the THREAD_POOL_EXECUTOR would implicitly be waited on while the HTTP client goes off, but I've used the IdlingResource here to accomplish everything nicely.

CoroutineContext.isActive is misleading because it checks if the context has a Job object and whether it is active. A CoroutineDispatcher is a context with no other elements like Job so it will always return false.

In order to track continuations you will probably need some sort of custom ContinuationInterceptor which keeps track of in-progress and cancelled continuations.

Related