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

When use liveData and change to flow by .asFlow() test won't trigger events #117

Open
mahdit8 opened this issue May 12, 2022 · 5 comments
Open

Comments

@mahdit8
Copy link

mahdit8 commented May 12, 2022

Hi
I have an issue when convert live data to flow like:

 fun foo(
    ): Flow<B> =
        bar().asFlow()
            .map { -> }
foo().test{
 val result = awaitItem()
 Assert.assertEquals(expected, result)
 awaitComplete()
}

Then I got this error
After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs: [ScopeCoroutine{Active}@35c3ef4a]
Previously I faced this issue and find out that in some case should awaitComplete() but it some case that will also not works.

@JakeWharton
Copy link
Collaborator

I have never used LiveData. Can you provide a self-contained failing test case?

@mahdit8
Copy link
Author

mahdit8 commented May 13, 2022

So we have some situation that live data is converted to flow in our useCases. In bellow test cases, test1 will fails but second will pass.

class TurbinFlowTest {

    private val flow1 = liveData {
        for (i in 1..10) {
            delay(100)
            emit(i)
        }

    }.asFlow()

    private val flow2 = flow {
        for (i in 1..10) {
            delay(100)
            emit(i)
        }

    }

    @Test
    fun `Should catch events when use flow that converted from live data with asFlow()`() = runTest {

        flow1.test{
            val item = awaitItem()
            Assert.assertNotNull(item)
            cancelAndIgnoreRemainingEvents()
        }

    }

    @Test
    fun `Should catch events when use flow direct constructor`() = runTest {

        flow2.test{
            val item = awaitItem()
            Assert.assertNotNull(item)
            cancelAndIgnoreRemainingEvents()
        }

    }
}

@mahdit8 mahdit8 changed the title Every time I use liveData and change to flow test wont trigger events When use liveData and change to flow test wont trigger events May 18, 2022
@mahdit8 mahdit8 changed the title When use liveData and change to flow test wont trigger events When use liveData and change to flow by .asFlow() test won't trigger events May 18, 2022
@mahdit8
Copy link
Author

mahdit8 commented May 20, 2022

Hey @JakeWharton I feel issue is some-how about coroutine scope here:

public fun <T> LiveData<T>.asFlow(): Flow<T> = flow {
    val channel = Channel<T>(Channel.CONFLATED)
    val observer = Observer<T> {
        channel.trySend(it)
    }
    withContext(Dispatchers.Main.immediate) {
        observeForever(observer)
    }
    try {
        for (value in channel) {
            emit(value)
        }
    } finally {
        GlobalScope.launch(Dispatchers.Main.immediate) {
            removeObserver(observer)
        }
    }
} 

And we should support this Main dispatcher in Turbine. If you gimme some hints to where to look into, may I can fix and create PR?

@NinoDLC
Copy link

NinoDLC commented Jul 6, 2022

I see no issue with Turbine. Your testing approach was missing some key components.

Both tests succeed here :

class TurbinFlowTest {

    // Needed to Unit Test LiveData
    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    // Some stuff from coroutine testing 1.6, check https://www.youtube.com/watch?v=tCliepyQfHQ for example for more information
    private val testCoroutineDispatcher = StandardTestDispatcher()
    private val testCoroutineScope = TestScope(testCoroutineDispatcher)

    private val flow1 = liveData {
        for (i in 1..10) {
            delay(100)
            emit(i)
        }
    }.asFlow()

    private val flow2 = flow {
        for (i in 1..10) {
            delay(100)
            emit(i)
        }
    }

    @Before
    fun setUp() {
        Dispatchers.setMain(testCoroutineDispatcher)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `Should catch events when use flow that converted from live data with asFlow()`() = testCoroutineScope.runTest {
        flow1.test {
            val item = awaitItem()
            Assert.assertNotNull(item)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun `Should catch events when use flow direct constructor`() = testCoroutineScope.runTest {
        flow2.test {
            val item = awaitItem()
            Assert.assertNotNull(item)
            cancelAndIgnoreRemainingEvents()
        }
    }
}

I recommend you take a look to this wonderful guide too : https://developer.android.com/kotlin/coroutines/coroutines-best-practices

@mahdit8
Copy link
Author

mahdit8 commented Aug 3, 2022

Thanks for response, for minimizing the question I did not pass dispatchers and as long as no dispatcher change I think runTest should be fine. So in my real code I'm doing something very same.
May missed part is InstantTaskExecutorRule
But I got error on your code:

Exception in thread "Test worker @coroutine#2" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
	at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.missing(MainDispatchers.kt:118)
	at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.isDispatchNeeded(MainDispatchers.kt:96)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:319)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:25)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:110)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
	at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
	at androidx.lifecycle.BlockRunner.maybeRun(CoroutineLiveData.kt:176)
	at androidx.lifecycle.CoroutineLiveData.onActive(CoroutineLiveData.kt:242)
	at androidx.lifecycle.LiveData.changeActiveCounter(LiveData.java:390)
	at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:466)
	at androidx.lifecycle.LiveData.observeForever(LiveData.java:234)
	at androidx.lifecycle.FlowLiveDataConversions$asFlow$1$1.invokeSuspend(FlowLiveData.kt:98)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:28)
	at kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTask(TestCoroutineScheduler.kt:95)
	at kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdle(TestCoroutineScheduler.kt:110)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:212)
	at kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invokeSuspend(TestBuilders.kt:167)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
	at kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:12)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:166)
	at kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest$default(TestBuilders.kt:161)
	at kotlinx.coroutines.test.TestBuildersKt.runTest$default(Unknown Source)
	at com.bitcoin.mwallet.core.usecases.TurbinFlowTest.Should catch events when use flow that converted from live data with asFlow()(TurbineTest.kt:53)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy5.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(3), "coroutine#3":StandaloneCoroutine{Cancelling}@328e4ec2, Dispatchers.Main[missing, cause=java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.]]
Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
	at android.os.Looper.getMainLooper(Looper.java)

Could you check if you see same 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

3 participants