I am writing unit tests for my viewModel, but having trouble executing the tests. The runBlocking { ... }
block doesn't actually wait for the code inside to finish, which is surprising to me.
The test fails because result
is null
. Why doesn't runBlocking { ... }
run the launch
block inside the ViewModel in blocking fashion?
I know if I convert it to a async
method that returns a Deferred
object, then I can get the object by calling await()
, or I can return a Job
and call join()
. But, I'd like to do this by leaving my ViewModel methods as void
functions, is there a way to do this?
// MyViewModel.kt class MyViewModel(application: Application) : AndroidViewModel(application) { val logic = Logic() val myLiveData = MutableLiveData<Result>() fun doSomething() { viewModelScope.launch(MyDispatchers.Background) { System.out.println("Calling work") val result = logic.doWork() System.out.println("Got result") myLiveData.postValue(result) System.out.println("Posted result") } } private class Logic { suspend fun doWork(): Result? { return suspendCoroutine { cont -> Network.getResultAsync(object : Callback<Result> { override fun onSuccess(result: Result) { cont.resume(result) } override fun onError(error: Throwable) { cont.resumeWithException(error) } }) } } }
// MyViewModelTest.kt @RunWith(RobolectricTestRunner::class) class MyViewModelTest { lateinit var viewModel: MyViewModel @get:Rule val rule: TestRule = InstantTaskExecutorRule() @Before fun init() { viewModel = MyViewModel(ApplicationProvider.getApplicationContext()) } @Test fun testSomething() { runBlocking { System.out.println("Called doSomething") viewModel.doSomething() } System.out.println("Getting result value") val result = viewModel.myLiveData.value System.out.println("Result value : $result") assertNotNull(result) // Fails here } }
Add a CoroutineScope to your ViewModel by creating a new scope with a SupervisorJob that you cancel in the onCleared() method. The coroutines created with that scope will live as long as the ViewModel is being used.
Launching a Coroutine as a JobThe launch {} function returns a Job object, on which we can block until all the instructions within the coroutine are complete, or else they throw an exception. The actual execution of a coroutine can also be postponed until we need it with a start argument. If we use CoroutineStart.
A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. Coroutines are useful here for when you have work that needs to be done only if the ViewModel is active.
To test code that includes suspend functions, you need to do the following: Add the kotlinx-coroutines-test test dependency to your app's build. gradle file. Annotate the test class or test function with @ExperimentalCoroutinesApi .
What you need to do is wrap your launching of a coroutine into a block with given dispatcher.
var ui: CoroutineDispatcher = Dispatchers.Main var io: CoroutineDispatcher = Dispatchers.IO var background: CoroutineDispatcher = Dispatchers.Default fun ViewModel.uiJob(block: suspend CoroutineScope.() -> Unit): Job { return viewModelScope.launch(ui) { block() } } fun ViewModel.ioJob(block: suspend CoroutineScope.() -> Unit): Job { return viewModelScope.launch(io) { block() } } fun ViewModel.backgroundJob(block: suspend CoroutineScope.() -> Unit): Job { return viewModelScope.launch(background) { block() } }
Notice ui, io and background at the top. Everything here is top-level + extension functions.
Then in viewModel you start your coroutine like this:
uiJob { when (val result = fetchRubyContributorsUseCase.execute()) { // ... handle result of suspend fun execute() here }
And in test you need to call this method in @Before block:
@ExperimentalCoroutinesApi private fun unconfinifyTestScope() { ui = Dispatchers.Unconfined io = Dispatchers.Unconfined background = Dispatchers.Unconfined }
(Which is much nicer to add to some base class like BaseViewModelTest)
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