Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
a-kari opened this issue Jun 30, 2020 · 0 comments

Comments

@a-kari
Copy link
Owner

a-kari commented Jun 30, 2020

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant