I am trying to test a simple ViewModel
with Robolectric
.
Here's my ViewModel
GreetingsViewModel.kt
@FlowPreview
@ExperimentalCoroutinesApi
class GreetingsViewModel : ViewModel() {
private val greetingsChannel = ConflatedBroadcastChannel<String>()
/**
* Whenever greetingsChannel offers a value, prepend `Hello ` with it and return.
*/
val greetingsResponse = greetingsChannel.asFlow()
.flatMapLatest { name ->
flowOf("Helo $name")
}.asLiveData(viewModelScope.coroutineContext)
/**
* To get new greetings
*/
fun askGreetings(name: String) {
greetingsChannel.offer(name)
}
}
and here's my unit test file
GreetingsViewModelTest.kt
@FlowPreview
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class GreetingsViewModelTest2 {
@Test
fun greeting_askGreetings_getWithName() {
val testViewModel = GreetingsViewModel()
testViewModel.askGreetings("john")
testViewModel.greetingsResponse.getOrAwaitValue().should.equal("Hello john")
}
}
But, when I run the test am getting below error.
java.lang.IllegalStateException: Cancel call cannot happen without a maybeRun
at androidx.lifecycle.BlockRunner.cancel(CoroutineLiveData.kt:184)
at androidx.lifecycle.CoroutineLiveData.onInactive(CoroutineLiveData.kt:245)
at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:440)
at androidx.lifecycle.LiveData.observeForever(LiveData.java:232)
at com.theapache64.twinkill.test.GetOrAwaitValueKt.getOrAwaitValue(getOrAwaitValue.kt:26)
at com.theapache64.twinkill.test.GetOrAwaitValueKt.getOrAwaitValue$default(getOrAwaitValue.kt:15)
at com.theapache64.tracktor.ui.activities.test.GreetingsViewModelTest.greeting_askGreetings_getWithName(GreetingsViewModelTest.kt:23)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:546)
at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:252)
at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Any idea why ?
NOTE: getOrAwaitValue() is an extension function to get the value of a LiveData
or waits for it to have one, with a timeout.
To avoid the IllegalStateException in Java, it should be ensured that any method in code is not called at an illegal or inappropriate time. Calling the next() method moves the Iterator position to the next element.
Signals that a method has been invoked at an illegal or inappropriate time. In other words, the Java environment or Java application is not in an appropriate state for the requested operation.
Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. You should look into one of your thread and make it synchronized with your UI thread. The way to do this in Android is with Handler.
IllegalStateException is the child class of RuntimeException and hence it is an unchecked exception.
This is a bug in LiveData library, and it should be fixed in version 2.3.0
You can test this fix in version 2.3.0-alpha05.
More details in the changelog and the issue tracker
https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.0-alpha05 https://issuetracker.google.com/issues/157840298
I was facing the same problem. As @Rahul Singhal was pointing the cause of the error was this line
[email protected](this)
So I decided to convert the LiveData back to flow and then get the value. This is the extension I'm using for getting the value now.
suspend fun <T> LiveData<T>.getFlowAsLiveDataValue(timeMillis: Long = 2_000): T? =
withTimeoutOrNull(timeMillis) {
[email protected]().first()
}
Your test then should look like
testViewModel.greetingsResponse.getFlowAsLiveDataValue().should.equal("Hello john")
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With