I have this code: `fun getFoo() : Flow<Int> ...
# coroutines
n
I have this code:
fun getFoo() : Flow<Int> = flowOf(1).flowOn(<http://Dispatchers.IO|Dispatchers.IO>)
How can I check during a Unit Test that the flow returned by
getFoo()
is running on the IO dispatcher ?
j
Is this a simplified example? What exact piece of code would you expect to run on the IO dispatcher here?
flowOn()
only affects the preceding part of the flow in the pipeline, not the collector, and here you create the value
1
before you even call
flowOn
anyway
n
Let's assume that instead of
flowOf(1)
, we are doing some IO stuff so using Dispatchers.IO is critical and should be validated... But I'm not sure how to validate that (I want my unit test to fail if I remove the
.flowOn(<http://Dispatchers.IO|Dispatchers.IO>)
j
This should probably tested on the boundary with your dependency on the IO stuff. If this code calls into something doing blocking IO, maybe replace that blocking IO dependency in the test with something that checks that it's running on the IO dispatcher
r
First the obvious: you should be injecting your Dispatcher rather than hardcoding
👍 2
n
Yes I do but it's even worse when injecting tbh
r
Second if you’re injecting a Dispatcher you should really inject CoroutineContext which is what flowOn actually uses
👍 1
Then if you have a custom Context controlled by your test it’ll likely be much easier to add a custom CoroutineName which you can later assert on
You’ll still need to be able to run asserts from inside the suspend code so hopefully this is something you can mock/ fake
n
Here's my simplified code if you need a more concrete example :
Copy code
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlin.coroutines.CoroutineContext

class FooRepository(
    private val fooDao: FooDao,
    private val ioDispatcher: CoroutineContext,
    private val defaultDispatcher: CoroutineContext,
) {
    fun getFooEntity(): Flow<Int> =
        fooDao.getFooDto() // IO intensive
            .flowOn(ioDispatcher)
            .map { it + it } // Simplified, but this is computationally intensive
            .flowOn(defaultDispatcher)
}

interface FooDao {
    fun getFooDto(): Flow<Int>
}

class FooRepositoryTest {

    // Some unit test run by JUnit for example
    fun test() {
        val testCoroutineDispatcher = StandardTestDispatcher()
        val testCoroutineScope = TestScope(testCoroutineDispatcher)

        val repository = FooRepository(
            fooDao = object : FooDao {
                override fun getFooDto(): Flow<Int> = flowOf(1)
            },
            ioDispatcher = testCoroutineDispatcher,
            defaultDispatcher = testCoroutineDispatcher,
        )

        testCoroutineScope.runTest { 
            // And now what ? :(
        }
    }
}
As per Kotlin recommandations, I'm using a StandardTestDispatcher with a TestScope, but the problem here is if I inject 2 "dispatchers" in my repository, they have to be the same AFAIK. IE, I can't create 2 StandardTestDispatchers...
j
Why not?
n
My TestScope only accepts one TestDispatcher (and that makes sense to me)
But I might be wrong there
j
As for the initial question, you're injecting the
FooDao
, so you could check the dispatcher that's in the context in your mock flow (if you use a slightly more complex flow with a body where you can perform the check)
My TestScope only accepts one TestDispatcher (and that makes sense to me)
Sure, your
TestScope
can only have one, but that doesn't have anything to do with the dispatchers you pass to
FooRepository
. What's preventing you from passing 2 different test dispatchers there?
more importantly, I'm not sure on which platform you're running this, but in general you should use
= runTest { ... }
to define your whole test body, which provides you with a test scope already, including a
testScheduler
. You can then reuse the same test scheduler to create other coroutine scopes if you need to
n
Yes I also had that idea to use a mock but it means I have to collect the flow in the test, only for the mock to be "run". Something like returning this in the mock :
Copy code
flow {
    assertThat(coroutineContext[CoroutineDispatcher]).isEqualTo(<http://Dispatchers.IO|Dispatchers.IO>)
}
But it doesn't help to check that part 1 is run on IO and part 2 is run on Default 😕
What's preventing you from passing 2 different test dispatchers there?
I always thought that dispatchers injected to the test class during UT should always be the TestDispatcher
j
They just have to use the same scheduler AFAIK (if you don't want problems with skipped delays)
n
I see... So something like that would be better ?
Copy code
val testCoroutineScheduler = TestCoroutineScheduler()
val ioDispatcher = StandardTestDispatcher(testCoroutineScheduler)
val defaultDispatcher = StandardTestDispatcher(testCoroutineScheduler)
val testCoroutineScope = TestScope(testCoroutineScheduler)

val repository = FooRepository(
    fooDao = object : FooDao {
        override fun getFooDto(): Flow<Int> = flow {
            require(coroutineContext[CoroutineDispatcher] == ioDispatcher)
        }
    },
    ioDispatcher = ioDispatcher,
    defaultDispatcher = defaultDispatcher,
)
j
Sort of, though as I said earlier you can (and should) use
runTest
at the root of your test function, and thus you have a
TestScope
and
testScheduler
already created for you
n
I can't remember exactly why but some time ago I had issues with this "simpler" approach and I had to create this JUnit rule in order for everything to work...
Copy code
class TestCoroutineRule : TestRule {

    val testCoroutineDispatcher = StandardTestDispatcher()
    private val testCoroutineScope = TestScope(testCoroutineDispatcher)

    override fun apply(base: Statement, description: Description): Statement = object : Statement() {
        @Throws(Throwable::class)
        override fun evaluate() {
            Dispatchers.setMain(testCoroutineDispatcher)

            base.evaluate()

            Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
        }
    }

    fun runTest(block: suspend TestScope.() -> Unit) = testCoroutineScope.runTest {
        block()
    }
Maybe the issues are fixed now, but I don't really want to migrate my whole project and later find the issues are back 😞
I really struggle to test the last
.flowOn(defaultDispatcher)
. It makes sense because the collection of the flow is done with the runTest coroutine context, not the flow's context (which is "applied upward"). Do you have any idea on how to validate the
.flowOn(defaultDispatcher)
part ?
r
you can inject a mock and do innovation testing . But i wonder if its worth test blob thinking upside down