Reputation: 2528
I'm using an old technique that no longer works on my viewmodel tests now for some reason.
In order the coroutine created via viewmodelScope.launch we need to reset the main dispatcher with a test dispatcher. I do this via MainDispatcherRule as described in https://developer.android.com/kotlin/coroutines/test#setting-main-dispatcher
@ExperimentalCoroutinesApi
class MainDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
My dependencies
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.9.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.activity:activity-ktx:1.6.0"
implementation "androidx.fragment:fragment-ktx:1.5.3"
// testing
// for runTest, CoroutineDispatcher
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
// for InstantTaskExecutorRule
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "io.mockk:mockk:1.13.2"
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
}
RestaurantsViewModel
@HiltViewModel
class RestaurantsViewModel @Inject constructor(private val repo: RestaurantsRepository): ViewModel() {
private val _restaurantsState = MutableLiveData<ResultState<List<Restaurant>>>()
val restaiurantsState: LiveData<ResultState<List<Restaurant>>> = _restaurantsState
init {
initialize()
}
fun initialize() {
viewModelScope.launch {
_restaurantsState.postValue(ResultState.Loading)
try {
_restaurantsState.postValue(
ResultState.Success(repo.getRestaurants())
)
} catch (ex: Exception) {
_restaurantsState.postValue(
ResultState.Failure("Failed to fetch restaurants", ex)
)
}
}
}
}
its associated test in RestaurantsViewModelTest
@ExperimentalCoroutinesApi
class RestaurantsViewModelTest {
private var repository = mockk<RestaurantsRepository>(relaxed = true)
private var viewmodel = RestaurantsViewModel(repository)
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun `restaurantsState LiveData is updated with the result from repository`() = runTest {
val restaurants = listOf(mockk<Restaurant>(relaxed = true))
val success = ResultState.Success(restaurants)
coEvery { repository.getRestaurants() } returns restaurants
viewmodel.initialize()
Assert.assertEquals(success, viewmodel.restaurantsState.value)
}
is met with the error described in Kotlin coroutine unit test fails with "Module with the Main dispatcher had failed to initialize"
Exception in thread "Test worker" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
My unit test compiles as is but fails to pass.
Upvotes: 0
Views: 1976
Reputation: 714
You should try initialise your view model in a setup function e.g
private lateinit var viewmodel:RestaurantsViewModel
@Before
fun setup() {
viewmodel = RestaurantsViewModel(repository)
}
Initialising your view model at the declaration site happens before the test rule executes, therefore the view model is using the main dispatcher.
Upvotes: 1