Nino
08/08/2023, 2:30 PMfun 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 ?Joffrey
08/08/2023, 2:32 PMflowOn()
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
anywayNino
08/08/2023, 2:34 PMflowOf(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>)
Joffrey
08/08/2023, 2:36 PMRobert Williams
08/08/2023, 3:02 PMNino
08/08/2023, 3:02 PMRobert Williams
08/08/2023, 3:02 PMRobert Williams
08/08/2023, 3:03 PMRobert Williams
08/08/2023, 3:04 PMNino
08/08/2023, 3:13 PMimport 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 ? :(
}
}
}
Nino
08/08/2023, 3:14 PMJoffrey
08/08/2023, 3:15 PMNino
08/08/2023, 3:15 PMNino
08/08/2023, 3:15 PMJoffrey
08/08/2023, 3:15 PMFooDao
, 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)Joffrey
08/08/2023, 3:16 PMMy 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?Joffrey
08/08/2023, 3:18 PM= 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 toNino
08/08/2023, 3:21 PMflow {
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 😕Nino
08/08/2023, 3:22 PMWhat'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
Joffrey
08/08/2023, 3:23 PMNino
08/08/2023, 3:29 PMval 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,
)
Joffrey
08/08/2023, 3:48 PMrunTest
at the root of your test function, and thus you have a TestScope
and testScheduler
already created for youNino
08/08/2023, 3:51 PMclass 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 😞Nino
08/08/2023, 4:37 PM.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 ?rkeazor
08/08/2023, 9:20 PM