I'm trying to test some code in my viewmodel. I'm trying to make two network calls with dispatcher async, but haven't figured out how to test. This is a simplified example of what I'm essentially trying to do,
// View Model Function
fun initialise() {
viewModelScope.launch(coroutineExceptionHandler) {
val accountDeferred = async(Dispatchers.IO) { accountModel.getAccounts() }
val contentDeferred = async(Dispatchers.IO) { contentModel.getContent() }
val account = accountDeferred.await()
val content = contentDeferred.await()
handleResult(account, content) // viewState is updated
}
}
// Unit Test
// Simplified
class Test {
@get:Rule
val coroutineRule = CoroutineTestRule(StandardTestDispatcher())
@Test
fun `This is the test`() {
runTest{
whenever(accountModel.getAccounts()).thenReturn(
Result.success( getAccountContent() , ReasonStatus.empty()))
whenever(contentModel.getContent()).thenReturn(
Result.success( getContent(), ReasonStatus.empty()))
// start view model
viewmodel = ViewModel(accountModel, contentModel)
runCurrent()
// In debugging mode, the variable viewState is set, before the two jobs in the viewmodel finish
val viewState = viewmodel.viewState.value
// check contents
assertNotEmpty(viewState.accounts)
// always fails since the network calls have not completed
}
}
}
What would be some suggestions to ensure that the two async awaits execute before the other code in runTest block executes first in a reliable manner?
You need to use dependency injection to provide IO dispatcher to your ViewModel
to make it testable. Then you can use the injected dispatcher instead of hard coding Dispatchers.IO
:
class ViewModel(
//...
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
//...
fun initialise() {
viewModelScope.launch(coroutineExceptionHandler) {
val accountDeferred = async(ioDispatcher) { accountModel.getAccounts() }
val contentDeferred = async(ioDispatcher) { contentModel.getContent() }
From the documentation on testing coroutines:
If the Main dispatcher has been replaced with a TestDispatcher, any newly-created TestDispatchers will automatically use the scheduler from the Main dispatcher, including the StandardTestDispatcher created by runTest if no other dispatcher is passed to it.
This makes it easier to ensure that there is only a single scheduler in use during the test. For this to work, make sure to create all other TestDispatcher instances after calling Dispatchers.setMain.
Assuming that CoroutineTestRule
sets the Main
dispatcher, you can now create a new TestDispatcher
to inject and it will use the same scheduler:
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `This is the test`() = runTest {
val dispatcher = StandardTestDispatcher()
val viewmodel = ViewModel(/*other args*/, dispatcher)
advanceUntilIdle()
//...
}