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;
}
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()
}