kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

Provide a way to determine if TestCoroutineDispatcher is "idle"

Open objcode opened this issue 6 years ago • 6 comments
trafficstars

When writing an advanced test that uses multiple related TestCoroutineDispatcher instances, there is a problem that can occur when one dispatches a task to another:

val dispatcherA = TestCoroutineDispatcher()
val dispatcherB = TestCoroutineDispatcher()

// inject those somehow to the class under test

@Test
fun test_withMultipleDispatchers() {
    dispatcherA.pause()
    dispatcherB.pause()
    
    foo()
    
    dispatcherA.runUntilIdle()
    dispatcherB.runUntilIdle()
    // executing tasks on B enqueued another task on A. Currently there is no way to determine if both of them are actually idle.
}

fun foo() {
    scope.launch(dispatcherA) {
        bar()
    }
}

fun bar() {
    scope.launch(dispatcherB) {
        foo()
    }
}

In this contrived example, there's an infinite recursion so it will never resolve, but this problem comes up in normal test code.

This also comes up in a more typical case of testing withContext. To test withContext is called, the easiest way is to inject a dispatcher to intercept the coroutine started by withContext. However, in the current API for TestCoroutineDispatcher there is no way to determine if a task has been sent to a dispatcher.

In addition, this sort of inspection is important for implementing an Espresso IdlingResource as discussed in https://github.com/Kotlin/kotlinx.coroutines/issues/242.

A bit of a complication here (since this should fit well with the Espresso integration which likely needs to work w/ regular dispatchers). A dispatcher has the ability to differentiate between two states without introducing extra tracking:

  1. Idle (all coroutines are suspended, or delayed into the future)
  2. Busy (some coroutine is currently running)

However, a TestCoroutineDispatcher can differentiate between three states

  1. Idle (all coroutines are suspended)
  2. Busy (some coroutine is currently running)
  3. DelayQueued(long) (some coroutine will resume after long delay)

No concrete API proposals at this second - wanted to make a issue to track this.

objcode avatar May 17 '19 00:05 objcode

But why mutliple test dispatchers are needed? Why one is not enough? What practical problem is being solved by using multiple test dispatchers?

elizarov avatar May 24 '19 00:05 elizarov

cc @yigit @manuelvicnt who have provided the feedback that they wanted use multiple dispatchers.

objcode avatar May 29 '19 22:05 objcode

why mutliple test dispatchers are needed?

When testing complex scenarios that involve multiple dispatchers (for example in Android, a ViewModel using main and computation), you might want to test the permutations (suspended, running, etc) of different coroutines running at the same time.

Obviously, you wouldn't inject multiple TestCoroutineDispatcher in all the tests, just in those covering these edge cases.

manuelvicnt avatar May 30 '19 09:05 manuelvicnt

I'm curious: Is this for testing possible race-conditions that could happen between UI coroutines running on the main dispatcher vs storage/networking coroutines running on the IO dispatcher in the actual app?

streetsofboston avatar Jun 06 '19 21:06 streetsofboston

But why mutliple test dispatchers are needed? Why one is not enough? What practical problem is being solved by using multiple test dispatchers?

@elizarov on Android Espresso UI tests, we are testing the application from within. The Android apps are running on different dispatchers to make sure the background work is not blocking the user from clicking. We want to test these apps from Espresso JUnit tests running on devices which have the concept of IdlingResources, which block every single assertion/action within Espresso: all registered idling resources have to be idle before anything is actioned/verified. This is to wait for background work to mutate the UI, similar to how a manual tester / user would wait for the app to settle (e.g. progress bars to disappear, results to appear) before continuing on. To implement this we could use a TestCoroutineDispatcher to listen for idle and an IdlingResource to block Espresso when not idle. We would wrap each of the IO/Main/Computation dispatchers individually. When running tests we want to get an environment as close to real as possible, so using a single dispatcher would mean we would be change real behavior and hiding potential threading issues, while all we want is to know "when things are done".

TWiStErRob avatar Jul 09 '21 10:07 TWiStErRob

Has there been any progress on this? I noticed the API already exists on TestCoroutineScheduler here, but it's marked internal. Any way that could just be marked public? That would solve my use case, where I want to validate that a call didn't schedule any work on a dispatcher.

TheKeeperOfPie avatar Apr 14 '25 19:04 TheKeeperOfPie