Coroutines - unit testing viewModelScope.launch methods
Solution 1
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)
Solution 2
As @Gergely Hegedus mentions above, the CoroutineScope needs to be injected into the ViewModel. Using this strategy, the CoroutineScope is passed as an argument with a default null
value for production. For unit tests the TestCoroutineScope will be used.
SomeUtils.kt
/**
* Configure CoroutineScope injection for production and testing.
*
* @receiver ViewModel provides viewModelScope for production
* @param coroutineScope null for production, injects TestCoroutineScope for unit tests
* @return CoroutineScope to launch coroutines on
*/
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
if (coroutineScope == null) this.viewModelScope
else coroutineScope
SomeViewModel.kt
class FeedViewModel(
private val coroutineScopeProvider: CoroutineScope? = null,
private val repository: FeedRepository
) : ViewModel() {
private val coroutineScope = getViewModelScope(coroutineScopeProvider)
fun getSomeData() {
repository.getSomeDataRequest().onEach {
// Some code here.
}.launchIn(coroutineScope)
}
}
SomeTest.kt
@ExperimentalCoroutinesApi
class FeedTest : BeforeAllCallback, AfterAllCallback {
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val repository = mockkClass(FeedRepository::class)
private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)
override fun beforeAll(context: ExtensionContext?) {
// Set Coroutine Dispatcher.
Dispatchers.setMain(testDispatcher)
}
override fun afterAll(context: ExtensionContext?) {
Dispatchers.resetMain()
// Reset Coroutine Dispatcher and Scope.
testDispatcher.cleanupTestCoroutines()
testScope.cleanupTestCoroutines()
}
@Test
fun topCafesPoc() = testDispatcher.runBlockingTest {
...
val viewModel = FeedViewModel(testScope, repository)
viewmodel.getSomeData()
...
}
}
Solution 3
As others mentioned, runblocking just blocks the coroutines launched in it's scope, it's separate from your viewModelScope. What you could do is to inject your MyDispatchers.Background and set the mainDispatcher to use dispatchers.unconfined.
Related videos on Youtube
Prem
Updated on February 12, 2022Comments
-
Prem over 2 years
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
isnull
. Why doesn'trunBlocking { ... }
run thelaunch
block inside the ViewModel in blocking fashion?I know if I convert it to a
async
method that returns aDeferred
object, then I can get the object by callingawait()
, or I can return aJob
and calljoin()
. But, I'd like to do this by leaving my ViewModel methods asvoid
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 } }
-
Andre Artus about 5 yearsPlease ensure that your question shows a Minimal, Complete, and Verifiable example (stackoverflow.com/help/mcve), it would make it easier to answer your question.
-
Vairavan about 5 yearsrunBlocking will only wait for child coroutines. Coroutines created using viewModelScope is not related to the scope inside runBlocking.
-
Vahab Ghadiri almost 4 yearsBetter approach is to pass dispatcher context to viewmodel, so you can pass your test dispather.. in your tests!
-
-
Prem about 5 yearsI don't think the issue is with the live data not propagating the value. If I change the executor in my viewmodel to a synchronous executor, then the test passes. So it definitely has something to do with the coroutines
-
Prem about 5 yearsAlso the
InstantTaskExecutorRule
rule makes sure livedata posts value instantly -
Andre Artus about 5 yearsDid you try it? InstantTaskExecutorRule only ensure the AAC run synchronously, you still need an observer.
-
Andre Artus about 5 years
-
AdamHurwitz about 4 yearsThanks @Gergely Hegedus. Craig Russell outlines a similar strategy. Do you have any simple samples to show how to organize the production/test-code?
-
AdamHurwitz about 4 yearsHere is a strategy for injecting
viewModelScope
into the ViewModel: How to inject viewModelScope for Android unit test with Kotlin coroutines?. -
USMAN osman about 4 years
Don’t use Dispatchers.Unconfined as a replacement of Dispatchers.Main, it will break all assumptions and timings for code that does use Dispatchers.Main. Since a unit test should run well in isolation and without any side effects, you should call Dispatchers.resetMain() and clean up the executor when the test finishes running.
reference: medium.com/androiddevelopers/… .. heading:Unit Testing viewModelScope
4th paragraph... -
Admin about 4 yearsthanks for the info. I am editing my answer as I found TestCoroutineDispatcher a better fit, and I am also applying rest after each test. I remember reading over that same blog you posted but it's kind of old now..
-
Edgar Khimich about 4 yearsYou are not allowed to use ui dispatcher inside the viewmodel scope Nothing related to the UI should be inside the ViewModel Read more how to use ViewModels
-
Jim Ovejera about 3 yearsNo
injectScope
for my ViewModel -
Igor about 2 yearsThe default dispatcher of the viewModeScope is already the Main Dispatcher @EdgarKhimich