You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Я пыталась написать интеграционный тест на фрагмент в отдельной ветке с помощью следующих инструментов:
JUnit 4
Robolectric
MockWebServer
kotlinx-coroutines-test
Суть проблемы
Т.к. я запускаю свой интеграционный тест на Robolectric, для теста я подменяю корутинный Dispatchers.Main на TestCoroutineDispatcher. Он до поры до времени работает нормально, но когда дело доходит до withContext {...}, возникает проблема:
Сначала корутина работает на Dispatchers.Main.
Затем переключается на Dispatchers.IO с помощью withContext(Dispatchers.IO) {...}.
После завершения функции withContext {...} корутина должна переключиться обрано на Dispatchers.Main, но этого не происходит - работа почему-то продолжается в Dispatchers.IO, на бэкграунд-потоке.
Подробное описание проблемы
WordFragmentIntegrationTest:
@RunWith(RobolectricTestRunner::class)
@Config(application =TestAppWithFacade::class)
classWordFragmentIntegrationTest {
// Подменяю Dispatchers.Main на TestCoroutineDispatcher.
@get:Rule
val coroutinesRule =CoroutinesRule()
privateval mockWebServer =MockWebServer()
@Before
funsetup() {
mockWebServer.start(8080)
}
@After
funteardown() {
mockWebServer.shutdown()
}
@Test
fun`should fetch a word from api and populate the view`() = runBlockingTest {
val response =MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(SAMPLE_API_WORD_JSON)
.setBodyDelay(0, TimeUnit.MILLISECONDS)
mockWebServer.enqueue(response)
// Запускаю WordFragment. Он сразу делает запрос к api через WordViewModel.val arguments =Bundle().apply { putString("wordId", SAMPLE_WORD_ID) }
val fragmentScenario = launchFragmentInContainer<WordFragment>(arguments)
.moveToState(Lifecycle.State.RESUMED)
// Дожидаюсь, пока WordFragment сделает запрос к api, и делаю проверки.
fragmentScenario.onFragment { fragmentUnderTest ->
runBlocking {
while (fragmentUnderTest.progressBar.visibility ==VISIBLE) { yield() }
// (Проверки на то, что UI правильно заполнился).
}
}
}
}
val wordLiveData = liveData {
// Блок liveData {...} запустился в UI-потоке, всё нормально.// Эмичу UIState.ShowLoading, чтобы показывался ProgressBar.
printCurrentThread("LiveData block is started on the UI thread")
emit(UIState.ShowLoading)
// Получаю данные в background-потоке, здесь тоже всё нормально.// Ответ успешно приходит с MockWebServer'a.val word = withContext(Dispatchers.IO) {
printCurrentThread("Fetching a word from api on a background thread")
resultMapper.mapToExternalLayer(loadWordUseCase(wordId))
}
// Вот здесь проблема. Функция withContext {...} после своего завершения должна переключиться// обратно на UI-поток, но она не переключается. Функция emit() вызывается в// background-потоке, что ведет к ошибке "Cannot invoke setValue on a background thread".//// Эта проблема касается только тестов, где я подменяю Dispatchers.Main на TestCoroutineDispatcher,// т.е. в основном коде приложения переключение на UI-поток происходит нормально.
printCurrentThread("withContext {...} has ended and should switch back to Dispatchers.Main, but it doesn't")
emit(word)
}
privatefunprintCurrentThread(message:String) {
val threadInfo ="Current thread id: ${Thread.currentThread().id}. UI thread id: ${Looper.getMainLooper().thread.id}"println("=================================================================================")
println(message)
println(threadInfo)
println("=================================================================================")
println()
println()
}
Логи:
=================================================================================
LiveData block is started on the UI thread
Current thread id: 11. UI thread id: 11
=================================================================================
=================================================================================
Fetching a word from api on a background thread
Current thread id: 19. UI thread id: 11
=================================================================================
=================================================================================
withContext {...} has ended and should switch back to Dispatchers.Main, but it doesn'tCurrent thread id: 19. UI thread id: 11=================================================================================Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IllegalStateException: Cannot invoke setValue on a background thread at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:462) at androidx.lifecycle.LiveData.setValue(LiveData.java:304) at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50) at androidx.lifecycle.LiveDataScopeImpl$emit$2.invokeSuspend(CoroutineLiveData.kt:99) at androidx.lifecycle.LiveDataScopeImpl$emit$2.invoke(CoroutineLiveData.kt) at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91) at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:160) at kotlinx.coroutines.BuildersKt.withContext(Unknown Source) at androidx.lifecycle.LiveDataScopeImpl.emit(CoroutineLiveData.kt:97) at jp.neechan.akari.dictionary.feature_word.presentation.WordViewModel$wordLiveData$1.invokeSuspend(WordViewModel.kt:51) (Coroutine boundary) at jp.neechan.akari.dictionary.feature_word.presentation.WordViewModel$wordLiveData$1.invokeSuspend(WordViewModel.kt:51) at androidx.lifecycle.BlockRunner$maybeRun$1.invokeSuspend(CoroutineLiveData.kt:176)Caused by: java.lang.IllegalStateException: Cannot invoke setValue on a background thread at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:462) at androidx.lifecycle.LiveData.setValue(LiveData.java:304) at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50) at androidx.lifecycle.LiveDataScopeImpl$emit$2.invokeSuspend(CoroutineLiveData.kt:99) at androidx.lifecycle.LiveDataScopeImpl$emit$2.invoke(CoroutineLiveData.kt) at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91) at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:160) at kotlinx.coroutines.BuildersKt.withContext(Unknown Source) at androidx.lifecycle.LiveDataScopeImpl.emit(CoroutineLiveData.kt:97) at jp.neechan.akari.dictionary.feature_word.presentation.WordViewModel$wordLiveData$1.invokeSuspend(WordViewModel.kt:51) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) at kotlinx.coroutines.test.internal.TestMainDispatcher.dispatch(MainTestDispatcher.kt:35) at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) at kotlinx.coroutines.DispatchedCoroutine.afterResume(Builders.common.kt:261) at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:113) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Предположение
Т.к. в основном коде приложения корутина WordViewModel работает нормально (переключается обратно на Dispatchers.Main после withContext(Dispatchers.IO) {...}), я предположила, что проблема в тестовом диспатчере. Т.е. либо у меня CoroutinesRule неправильный, либо тестовый диспатчер пока нестабилен.
Я пыталась написать интеграционный тест на фрагмент в отдельной ветке с помощью следующих инструментов:
Суть проблемы
Т.к. я запускаю свой интеграционный тест на Robolectric, для теста я подменяю корутинный
Dispatchers.Main
наTestCoroutineDispatcher
. Он до поры до времени работает нормально, но когда дело доходит доwithContext {...}
, возникает проблема:Dispatchers.Main
.Dispatchers.IO
с помощьюwithContext(Dispatchers.IO) {...}
.withContext {...}
корутина должна переключиться обрано наDispatchers.Main
, но этого не происходит - работа почему-то продолжается вDispatchers.IO
, на бэкграунд-потоке.Подробное описание проблемы
WordFragmentIntegrationTest:
CoroutinesRule:
WordViewModel:
Логи:
Предположение
Т.к. в основном коде приложения корутина
WordViewModel
работает нормально (переключается обратно наDispatchers.Main
послеwithContext(Dispatchers.IO) {...}
), я предположила, что проблема в тестовом диспатчере. Т.е. либо у меняCoroutinesRule
неправильный, либо тестовый диспатчер пока нестабилен.Я написала issue в тестовую библиотеку корутин.
The text was updated successfully, but these errors were encountered: