Ожидал
Какова лучшая стратегия внедрения
viewModelScope
для модульных тестов Android с сопрограммами Kotlin?Когда CoroutineScope вводится в ViewModel для модульных тестов, следует ли также вводить и определять CoroutineDispatcher с помощью
flowOn
, даже если он не нужен в производственном коде?
flowOn
не требуется в производственном коде в этом случае использования, поскольку Retrofit обрабатывает потоки на Dispatchers.IO
в SomeRepository.kt, а viewModelScope
возвращает данные на Dispathers.Main
, оба по умолчанию.
Наблюдаемый
Запустите модульный тест на значениях состояния представления ViewModel Android, сохраненных в значении Kotlin Flow.
Реализация
Не удалось инициализировать модуль с главным диспетчером. Для тестов можно использовать Dispatchers.setMain из модуля kotlinx-coroutines-test
Модульный тест завершается ошибкой в первом случае, когда CoroutineScope жестко запрограммирован. viewModelScope
используется для того, чтобы запущенная сопрограмма поддерживала жизненный цикл ViewModel. Однако viewModelScope
создается из ViewModel, что делает его более сложным для внедрения по сравнению с CoroutineDispatcher, который может быть определен вне ViewModel и передан в качестве аргумента.
Полный журнал ошибок
SomeViewModel.kt
fun bindIntents(view: FeedView) {
view.initStateIntent().onEach {
initState(view)
}.launchIn(viewModelScope)
}
SomeTest.kt
@ExperimentalCoroutinesApi
class SomeTest : 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 {
coEvery {
repository.getInitialCafes(any())
} returns mockGetInitialCafes(mockCafesList, SUCCESS)
val viewModel = FeedViewModel(repository)
viewModel.bindIntents(object : FeedView {
@ExperimentalCoroutinesApi
override fun initStateIntent() = MutableStateFlow(true)
@ExperimentalCoroutinesApi
override fun loadNetworkIntent() = loadNetworkIntent.filterNotNull()
override fun render(viewState: FeedViewState) {
// TODO: Test viewState
}
})
loadNetworkIntent.value = LoadNetworkIntent(true)
// TODO
// assertEquals(4, 2 + 2)
}
}
Примечание. В окончательной версии будет использоваться тестовое расширение JUnit 5.
В производстве ViewModel создается с нулевым viewModelScope
, поскольку используется flowOn
в ViewModel. Для тестирования flowOn
передается как аргумент ViewModel.
Вопросов