Hi everyone! How <Flow.sample()> should be tested ...
# coroutines
a
Hi everyone! How Flow.sample() should be tested with kotlinx-coroutines-test 1.6.0? Every time I run attached test it runs infinitely 🙈
1
Same case without references to android specific code. lib versions from build.gradle:
Copy code
testImplementation("io.mockk:mockk:1.12.2")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0")
b
Unfortunately, there is no easy way to do this. The problem is you're launching infinite coroutine (collecting sharedflow never ends). The only option for you is to cancel the viewModelScope once you finished your test
a
Unfortunately no 😞 There is no problem testing infinite coroutine as you can see in attached file
b
so you have to do:
Copy code
val scope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) }
val viewModel by lazy { .... }

.....
fun `test vm`() = runTest {
   vm.push()
   runCurrent()
   verify...
   
   scope.cancel()
}
ah yes, you're passing a different job to the scope, so most likely you need to replace runCurrent with advanceBy + runCurrent but note, since you're passing different job the VM, it may leak once the test finishes
a
still getting infinite runs 😢 IMHO
sample()
somehow blocks
runTest
and preventing it from terminating 🤔
n
There is no problem testing infinite coroutine as you can see in attached file
Your code also passes just fine if
push
launches a job that throws an exception after emitting. The problem is there, your test code just isn't detecting it.
IMHO 
sample()
 somehow blocks 
runTest
 and preventing it from terminating 🤔
Sample creates a repeating timer, so even though your test is poorly setup, it's still able to detect that something is scheduled so you've leaked a coroutine. Give your tested code a CoroutineScope that is connected to your TestScope. That way it can detect infinite coroutines and also other errors. If you have infinite coroutines, give your tested code a child scope and cancel that. And if you don't use Main, then you don't need to set main. Here's an example:
Copy code
class SomeTest {

    private val testScope =  TestScope()
    val viewModelScope = testScope + Job(testScope.coroutineContext.job)
    val viewModel = SomeViewModel(viewModelScope)

    @Test
    fun `test vm`() = testScope.runTest {
        viewModel.push()
        runCurrent()
        viewModelScope.cancel()
        verify(exactly = 1) { viewModel.drop() }
    }
}
And if you really are checking if
viewModel.someMethod()
causes
viewModel.someOtherMethod()
to be called (as opposed to it just being for the example), I would suggest you re-evaluate how you are writing your tests. I suspect either your class should be split up or your tests are fragile and will constantly break even just from changing minor details of the implementation. Do you really want to rewrite unit tests even for changes that have no behavior change from a public API perspective?