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

kotlinx-coroutines-test. withContext {...} in liveData {...} doesn't switch back to the previous context after its finish #2102

Closed
a-kari opened this issue Jun 22, 2020 · 3 comments
Assignees

Comments

@a-kari
Copy link

@a-kari a-kari commented Jun 22, 2020

Hello! I'm writing an integration test for my Android app. And I've faced an issue with liveData {...} coroutine builder: when I call withContext {...} function inside of it, it switches to a given context (e.g. Dispatchers.IO), but does not switch back after return (to Dispatchers.Main.immediate).

The test looks like:

WordFragmentIntegrationTest:

@RunWith(RobolectricTestRunner::class)
@Config(application = TestAppWithDaggerComponent::class)
class WordFragmentIntegrationTest {

    @get:Rule
    val coroutinesRule = CoroutinesRule()

    private val mockWebServer = MockWebServer()

    @After
    fun teardown() {
        mockWebServer.shutdown()
    }

    @Test
    fun `should fetch a word from api and populate the view`() = runBlockingTest {
        // Mock api response, it works fine.
        val response = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
                                     .setBody(SAMPLE_API_WORD_JSON)
                                     .setBodyDelay(0, TimeUnit.MILLISECONDS)
        mockWebServer.enqueue(response)
        mockWebServer.start(8080)

        // Launch a Fragment under test. It triggers an api call.
        val fragmentScenario = launchFragmentInContainer<WordFragment>()

        // Wait for the Fragment to do its work.
        fragmentScenario.onFragment { fragmentUnderTest ->
            runBlocking {
                while (fragmentUnderTest.isLoading) { yield() }
                
                // Do some assertions...
            }
        }
    }
}

CoroutinesRule:

class CoroutinesRule : ExternalResource() {

    override fun before() {
        Dispatchers.setMain(TestCoroutineDispatcher())
    }

    override fun after() {
        Dispatchers.resetMain()
    }
}

And a piece of code, which causes test failure.

WordViewModel:

val wordLiveData = liveData {
    printCurrentThread("Emitting the first value")
    emit(UIState.ShowLoading)

    val value = withContext(Dispatchers.IO) {
        printCurrentThread("Fetching a value")
        loadWordUseCase(wordId)
    }

    printCurrentThread("Emitting the second value")
    emit(value)
}

private fun printCurrentThread(message: String) {
    val threadInfo = "Thread: ${Thread.currentThread().id}. UI thread: ${Looper.getMainLooper().thread.id}"
    println("$message. $threadInfo")
}

It works fine in production environment:

Emitting the first value. Thread: 1. UI thread: 1  # First emitting is on the UI thread.
Fetching a value. Thread: 365. UI thread: 1        # Fetching is on some IO thread.
Emitting the second value. Thread: 1. UI thread: 1 # Switched back to the UI thread.

But in the test environment (where Dispatchers.Main is replaced) withContext {...} does not switch back to liveData {...}'s Dispatchers.Main.immediate, and it causes CoroutineLiveData crash, because its emit() should be called from the UI thread.

Emitting the first value. Thread: 11. UI thread: 11  # Emitted on the UI thread.
Fetching a value. Thread: 19. UI thread: 11          # Switched to IO thread.
Emitting the second value. Thread: 19. UI thread: 11 # Didn't switch back, which caused:

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)
...

Is it a bug or am I doing something wrong? E.g. maybe I have a wrong CoroutinesRule?

@a-kari a-kari changed the title kotlinx-coroutines-test. withContext {...} in liveData{...} doesn't switch back to the previous context after its finish kotlinx-coroutines-test. withContext {...} in liveData {...} doesn't switch back to the previous context after its finish Jun 22, 2020
@elizarov elizarov self-assigned this Jul 10, 2020
@elizarov
Copy link
Member

@elizarov elizarov commented Jul 10, 2020

It looks like some problem in TestCoroutineDispatcher (cc @qwwdfsad ) that sometimes seems to behave like an unconfined dispatcher. I struggle to reproduce your specific setup, though.

@a-kari
Copy link
Author

@a-kari a-kari commented Jul 10, 2020

Updated: There is a minimal reproducing project.


My real project setup is in this branch.

Files to check:

@aconda-mercari
Copy link

@aconda-mercari aconda-mercari commented Oct 8, 2021

Is there an update to this?

dkhalanskyjb added a commit that referenced this issue Nov 1, 2021
Defines two test dispatchers:
* StandardTestDispatcher, which, combined with runTest,
  gives an illusion of an event loop;
* UnconfinedTestDispatcher, which is like
  Dispatchers.Unconfined, but skips delays.

By default, StandardTestDispatcher is used due to the somewhat
chaotic execution order of Dispatchers.Unconfined.
TestCoroutineDispatcher is deprecated.

Fixes #1626
Fixes #1742
Fixes #2082
Fixes #2102
Fixes #2405
Fixes #2462
dkhalanskyjb added a commit that referenced this issue Nov 17, 2021
Defines two test dispatchers:
* StandardTestDispatcher, which, combined with runTest,
  gives an illusion of an event loop;
* UnconfinedTestDispatcher, which is like
  Dispatchers.Unconfined, but skips delays.

By default, StandardTestDispatcher is used due to the somewhat
chaotic execution order of Dispatchers.Unconfined.
TestCoroutineDispatcher is deprecated.

Fixes #1626
Fixes #1742
Fixes #2082
Fixes #2102
Fixes #2405
Fixes #2462
dkhalanskyjb added a commit that referenced this issue Nov 17, 2021
Defines two test dispatchers:
* StandardTestDispatcher, which, combined with runTest,
  gives an illusion of an event loop;
* UnconfinedTestDispatcher, which is like
  Dispatchers.Unconfined, but skips delays.

By default, StandardTestDispatcher is used due to the somewhat
chaotic execution order of Dispatchers.Unconfined.
TestCoroutineDispatcher is deprecated.

Fixes #1626
Fixes #1742
Fixes #2082
Fixes #2102
Fixes #2405
Fixes #2462
dkhalanskyjb added a commit that referenced this issue Nov 19, 2021
Defines two test dispatchers:
* StandardTestDispatcher, which, combined with runTest,
  gives an illusion of an event loop;
* UnconfinedTestDispatcher, which is like
  Dispatchers.Unconfined, but skips delays.

By default, StandardTestDispatcher is used due to the somewhat
chaotic execution order of Dispatchers.Unconfined.
TestCoroutineDispatcher is deprecated.

Fixes #1626
Fixes #1742
Fixes #2082
Fixes #2102
Fixes #2405
Fixes #2462
yorickhenning pushed a commit to yorickhenning/kotlinx.coroutines that referenced this issue Jan 28, 2022
This commit introduces the new version of the test module.
Please see README.md and MIGRATION.md for a thorough
discussion of the changes.

Fixes Kotlin#1203
Fixes Kotlin#1609
Fixes Kotlin#2379
Fixes Kotlin#1749
Fixes Kotlin#1204
Fixes Kotlin#1390
Fixes Kotlin#1222
Fixes Kotlin#1395
Fixes Kotlin#1881
Fixes Kotlin#1910
Fixes Kotlin#1772
Fixes Kotlin#1626
Fixes Kotlin#1742
Fixes Kotlin#2082
Fixes Kotlin#2102
Fixes Kotlin#2405
Fixes Kotlin#2462

Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

4 participants