a-kari / dictionary Goto Github PK
View Code? Open in Web Editor NEWAn explanatory dictionary of English words.
An explanatory dictionary of English words.
Я пыталась написать интеграционный тест на фрагмент в отдельной ветке с помощью следующих инструментов:
Т.к. я запускаю свой интеграционный тест на 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)
class WordFragmentIntegrationTest {
// Подменяю Dispatchers.Main на TestCoroutineDispatcher.
@get:Rule
val coroutinesRule = CoroutinesRule()
private val mockWebServer = MockWebServer()
@Before
fun setup() {
mockWebServer.start(8080)
}
@After
fun teardown() {
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 правильно заполнился).
}
}
}
}
CoroutinesRule:
class CoroutinesRule : ExternalResource() {
override fun before() {
Dispatchers.setMain(TestCoroutineDispatcher())
}
override fun after() {
Dispatchers.resetMain()
}
}
WordViewModel:
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)
}
private fun printCurrentThread(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't
Current 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
неправильный, либо тестовый диспатчер пока нестабилен.
Я написала issue в тестовую библиотеку корутин.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.