https://kotlinlang.org logo
#android
Title
# android
a

Akram Raza

03/08/2024, 5:24 PM
The doc clearly specifies the behaviour.
e

Evghenii

03/08/2024, 5:27 PM
Let me update the test with an example that still fails. I do yield the test thread, because the while loop coroutine does get executed. If I remove the while coroutine, then it all works. My problem is only when both coroutines are there.
Copy code
@Test
    fun someTest() = runTest {
        mockkObject(OtherObject)
        every { OtherObject.someSharedFlow } returns testSharedFlow.asSharedFlow()

        mockkObject(ThirdObject)
        every { ThirdObject.doSomeStuff() } answers {}

        val myClass = MyClass(StandardTestDispatcher(testScheduler))
        myClass.start()
        advanceTimeBy(100L)

        testSharedFlow.emit(SomeEvent()).also {
            advanceTimeBy(100L)
            verify { ThirdObject.doSomeStuff() } // this fails
        }
        
        myClass.stop() // stops the coroutines

        unmockkObject(ThirdObject)
        unmockkObject(OtherObject)
    }
a

Akram Raza

03/08/2024, 5:32 PM
Not talking about your test. I am talking about your code for while loop. So here's what is happening. You are launching first coroutine with while loop running infinitely. Now your first coroutine test in running infinitely and so your test thread is never free to move onto your second coroutine.
And now here's an example test for you, change your while loop of first coroutine like this. //First coroutine launch{ var i=0 while(i<4){ //Do some stuff delay(1000L) i++ } } //Second coroutine And also advanceTimeBy(1001) in your test because the delay is 1000. And since this loop will run 4 times advanceTimeBy(4001) because of 4 delays. And now run it. Both coroutines will get tested.
Coroutines are queued using dispatcher. And I guess you know what queued means. One after another. It's not like run everything together. And delay is skipped in runTest , it doesn't mean that a never ending while loop will be automatically jumped out of by the test.
e

Evghenii

03/08/2024, 7:19 PM
Sorry, couldn't answer sooner.
And delay is skipped in runTest
So that's why it happens. I still hoped that a delay will move current coroutine to the end of the queue, even if it doesn't wait, but apparently it doesn't? Looks like a potential improvement to me. Anyway, I need that coroutine to run until the coroutine is stopped from outside. I'll think of a refactor to make it testable.
Thanks
a

Akram Raza

03/08/2024, 8:09 PM
It doesn't wait because delays are skipped. And they are skipped to make the tests run quickly. It has nothing to do with the queue.
e

Evghenii

03/08/2024, 9:23 PM
I understand why it doesn't wait, but it doesn't jump to other coroutines either like it would outside of tests
a

Akram Raza

03/09/2024, 3:36 AM
I think you have misunderstood coroutines and flows a little. Nobody is jumping nowhere , neither in test nor in business logic. In business logic you can have multiple coroutines running at once but not in tests. In business logic delay is just suspending your first coroutine for 1000ms for every loop run that's it. It doesn't switch between coroutines. If you remove the delay you will still be collecting data in your second coroutine whenever it is emitted, that's how flows work. I mean if you really want two different tasks to be done one after another then what's the benefit of going asynchronous with 2 coroutines ? You might as well just put both your tasks in one single coroutine to be executed one after another. Here is what you can do to understand this. In your business logic, move both launch inside one launch under the scope. Save the first coroutine launch to a job. And join this job after first launch before second coroutine launch. Now no matter what you do , whatever delay you provide, you won't be collecting anything in second coroutine. But according to you the delay should allow it. But still why nothing is getting collected in second coroutine ? Because now you are stuck waiting for the first coroutine to finish before your second coroutine gets launched. And it will never finish because of your never ending while loop and so your second coroutine will never get launched and you will collect nothing. And this is the exact case with testing.
e

Evghenii

03/09/2024, 8:44 PM
I think you misunderstood my understanding too, or maybe I didn't express it well enough. In your last paragraph, sure the second code piece will never execute, cause it's all within same coroutine. But my code has multiple coroutines, and delay is supposed to suspend current coroutine and release the thread to execute other coroutines that are ready. Here, I made a small program that runs three coroutines in a single thread. The first one emulates my test and launches another coroutine with a while loop and delay, and a coroutine that collects flow events.
Copy code
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineScope

val scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
val sharedFlow = MutableSharedFlow<Int>()

fun main() {
    val job = scope.launch {
        val whileJob = scope.launch {
            var i = 0
            while (i < 10) {
                println("While loop $i,    thread ${Thread.currentThread().getName()}")
                delay(100L)
                i++
            }
        }

        scope.launch {
            sharedFlow.collect { i ->
                println("Shared flow $i,   thread ${Thread.currentThread().getName()}")
            }
        }

        repeat(10) { i->
            sharedFlow.emit(i)
            delay(50L)
        }
        
        whileJob.join()
    }

    runBlocking {
        job.join()
    }
}
All get executed in the order I expect them to
Copy code
While loop 0,    thread DefaultDispatcher-worker-1
Shared flow 1,   thread DefaultDispatcher-worker-1
Shared flow 2,   thread DefaultDispatcher-worker-1
While loop 1,    thread DefaultDispatcher-worker-1
Shared flow 3,   thread DefaultDispatcher-worker-1
Shared flow 4,   thread DefaultDispatcher-worker-1
While loop 2,    thread DefaultDispatcher-worker-1
Shared flow 5,   thread DefaultDispatcher-worker-1
Shared flow 6,   thread DefaultDispatcher-worker-1
While loop 3,    thread DefaultDispatcher-worker-1
Shared flow 7,   thread DefaultDispatcher-worker-1
Shared flow 8,   thread DefaultDispatcher-worker-1
While loop 4,    thread DefaultDispatcher-worker-1
Shared flow 9,   thread DefaultDispatcher-worker-1
While loop 5,    thread DefaultDispatcher-worker-1
While loop 6,    thread DefaultDispatcher-worker-1
While loop 7,    thread DefaultDispatcher-worker-1
While loop 8,    thread DefaultDispatcher-worker-1
While loop 9,    thread DefaultDispatcher-worker-1
But that's because this is not a test dispatcher, and so delay does release the thread, and the thread does run other coroutines whenever they are ready.
I expected delay to release the thread in test environment too and jump to other coroutines
Okay I figured it out. My first code sample is working correctly. The problem was that there was an Android system call inside the while loop, and I didn't mock that. So the coroutine was crashing.