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;
}
CodePudding user response:
What I ended up doing is wrapping the Dispatcher in an interface.
interface IDispatcherProvider{
val main : Dispatcher
val io : Dispatcher
val default: Dispatcher
val unconfined: Dispatcher
}
This allows the deployment of your code to have :
object DispatcherProvider : IDispatcherProvider{
val main : Dispatcher = Dispatchers.Main
val io : Dispatcher = Dispatchers.IO
val default: Dispatcher = Dispatchers.Default
val unconfined: Dispatcher = Dispatchers.Unconfined
}
Then you pass around the interface so that you can inject the TestProvider:
object TestDispatcherProvider : IDispatcherProvider{
val main : Dispatcher = TestCoroutineDispatcher()
val io : Dispatcher = TestCoroutineDispatcher()
val default: Dispatcher = TestCoroutineDispatcher()
val unconfined: Dispatcher = TestCoroutineDispatcher()
}
This way your test will not use the thread pool but will run sequentially. There is another way around this that follows the same premise were you reassign the dispatcher, but I cannot remember what it is and this is very close (used a DI to pass around) to what we did and has been working well for our coroutine test. So in your class :
class SomeClass(private val dispatcher: IDispatcherProvider = DispatcherProvider){
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(dispatcher.main) {
delay(1000)
liveData1.postValue(true)
delay(1000)
liveData1.postValue(false)
}
}
}
CodePudding user response:
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()
}
}
}
