Git Product home page Git Product logo

dictionary's People

Contributors

a-kari avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

dictionary's Issues

Проблема с TestCoroutineDispatcher в интеграционном тесте

Я пыталась написать интеграционный тест на фрагмент в отдельной ветке с помощью следующих инструментов:

  • 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)
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 в тестовую библиотеку корутин.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.