androidkotlinandroid-jetpack-composekotlin-coroutinesandroid-espresso

In an Espresso test, how can I wait for coroutines launch()ed by a scope from rememberCoroutineScope()?


My Android app is using Jetpack Compose. Inside one of my @Composable functions, I get a coroutine scope:

@Composable
fun MyComposable() {
    val scope = rememberCoroutineScope()
    // ...

Then, I use it in one of my buttons' onClick to run a suspend function:

    Button(onClick = {
        scope.launch {
            doThings()
        }
    }) {
        // ...
    }
}

suspend fun doThings() {
    // ...
}

I'd like to do an Espresso test of the click's results. However, immediately after

onNode(/* ... */).performClick()

doThings() isn't done yet. I'd like to wait until doThings() finishes, without using Thread.sleep(), something like:

waitUntil { theThingsAreDone() }

Is there any way I can do this without putting the test code in my production code?

If I could replace scope in my test, then I could use one that incremented and decremented a CountingIdlingResource, thus making Espresso wait for it automatically. Passing scope as a parameter to MyComposable might give me that control, but then I'd lose the behavior of rememberCoroutineScope(). Can I modify scope in my test, while having it still follow the behavior of rememberCoroutineScope()?


Solution

  • The solution I went with was to use CountingIdlingResource from expresso-idling-resource (documentation), so that I could use composeRule.waitForIdle(). I wrote the following helper function

    val idlingResource = CountingIdlingResource("MainActivity", true)
    
    inline fun CoroutineScope.launchIdling(crossinline block: suspend CoroutineScope.() -> Unit): Job {
        idlingResource.increment()
        var job: Job? = null
        try {
            job = launch {
                try {
                    block()
                } finally {
                    idlingResource.decrement()
                }
            }
            return job
        } finally {
            if (job == null) idlingResource.decrement()
        }
    }
    

    and a specialized version of it which was useful for the code I was writing. Then I used scope.launchIdling instead of scope.launch in my code, and in my tests:

    @Before
    fun registerIdlingResource() {
        IdlingRegistry.getInstance().register(idlingResource)
    }
    
    @After
    fun unregisterIdlingResource() {
        IdlingRegistry.getInstance().unregister(idlingResource)
    }
    

    and waitForIdle() worked.