Search code examples
androidkotlindelaykotlin-coroutines

Testing Function with Multiple Coroutine Delays


Could someone please advise how to assert on lines of code which are behind coroutine delays in functions. I am unable to do this using injected dispatchers and using runBlockingTest. I have also updated my projects dependency and tried using the newer runTest to no avail.

Please could someone advise.

Code Example:

val liveData1 = MutableLiveData(false)

fun foo() {
    doTheThing(liveData1, {lambda1(liveData1)})
}

fun doTheThing(liveData1: LiveData<Boolean>, f1: () -> Unit) {
    if (!liveData1.value) {
        f1()
    } 
}

fun lambda1(liveData1: LiveData<Boolean>) {
    viewModelScope.launch(dispatchers.main) {
        delay(1000)
        liveData1.postValue(true)
        delay(1000)
        liveData1.postValue(false)
    }
}

Test Example:

@ExperimentalCoroutinesApi
@Test `test doTheThing`() = runBlockingTest{
    val subject = MyClass(TestCoroutineDispatchers())
    
    val observer1 = subject.liveData1.test()

    observers1.assertValueHistory(false)
    
    subject.foo()

    observers1.assertValueHistory(false, true, false) // fails here stating should have history [false]!=[false, true, false]
}

I have checked this and if I set the delays as 0, then my assertions are correct. I have gone through the debugger and the tests always runs the code down to the first delay, but never reaches the code past the delay.

LiveData testing helper functions:

fun <T> LiveData<T>.test(): TestObserver<T> = TestObserver.test(this)

********

public TestObserver<T> assertValueHistory(T... values) {
    List<T> mValueHistory = valueHistory();
    int size = mValueHistory.size();
    if (size != values.length) {
        throw fail("Value count differs; expected: " + values.length + " " + Arrays.toString(values)
                + " but was: " + size + " " + this.valueHistory);
    }

    for (int valueIndex = 0; valueIndex < size; valueIndex++) {
        T historyItem = mValueHistory.get(valueIndex);
        T expectedItem = values[valueIndex];
        if (notEquals(expectedItem, historyItem)) {
            throw fail("Values at position " + valueIndex + " differ; expected: " + valueAndClass(expectedItem) + " but was: " + valueAndClass(historyItem));
        }
    }

    return this;
}

Solution

  • I figured it out. You need to use the following dispatcher. The TestCoroutineDispatcher() alone isn't enough...

    @ExperimentalCoroutinesApi
    @InternalCoroutinesApi
    object SynchronousDispatchersWithNoDelay : Dispatchers {
    
        override val io: CoroutineDispatcher
            get() = NoDelayDispatcher()
    
        override val main: CoroutineDispatcher
            get() = NoDelayDispatcher()
    
        class NoDelayDispatcher : CoroutineDispatcher(), Delay {
    
            override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) { 
                continuation.resume(Unit) {} 
            }
    
            override fun dispatch(context: CoroutineContext, block: Runnable) {
                block.run()
            }
        }
    }
    

    Edit:

    Dispatchers is an interface I defined. I already has SynchronousDispatcher for testing purposes, but it didn't handle the delay in testing issue I had. The interface and other implementations are below:

    Interface:

    interface Dispatchers {
    
        val io: CoroutineDispatcher
        val main: CoroutineDispatcher
    }
    

    Implementation for use in code:

    object DispatchersImpl : Dispatchers {
    
        override val io: CoroutineDispatcher
            get() = kotlinx.coroutines.Dispatchers.IO
    
        override val main: CoroutineDispatcher
            get() = kotlinx.coroutines.Dispatchers.Main
    }
    

    Implementation for use in tests: This is the one I tried to use first which I describe above as "not enough"

    @ExperimentalCoroutinesApi
    object SynchronousDispatchers : Dispatchers {
    
        override val io: CoroutineDispatcher
            get() = TestCoroutineDispatcher()
    
        override val main: CoroutineDispatcher
            get() = TestCoroutineDispatcher()
    }