Lukasz Kalnik
02/07/2022, 6:06 PM@Test
fun `test flow`() = runTest {
val eventUpdateService = mockk {
every { subscribeToEvents() } returns flowOf(SomeEvent).onStart { delay(1000) }
}
val presenter = Presenter(
UnconfinedTestDispatcher(),
eventUpdateService
)
presenter.attachView(view)
advanceTimeBy(2000)
verify { view.displayEvent() } // fails because the flow didn't emit
}
class Presenter(
coroutineContext: CoroutineContext,
val eventUpdateService: EventUpdateService
) {
val coroutineScope = CoroutineScope(coroutineContext)
fun attachView(view: View) {
coroutineScope.launch {
eventUpdateService.subscribeToEvents().collect { event ->
view.displayEvent()
}
}
}
}
UnconfinedTestDispatcher
for the CoroutineScope
to collect the flow.Joffrey
02/07/2022, 6:08 PMLukasz Kalnik
02/07/2022, 6:09 PMJoffrey
02/07/2022, 6:09 PMpresenter.subscribeToEvents()
is mocked to return a flow that is never used or collected, what is inside the presenter is irrelevantLukasz Kalnik
02/07/2022, 6:10 PMJoffrey
02/07/2022, 6:23 PMadvanceTimeBy
call doesn't affect the Presenter
because you're passing a new UnconfinedTestDispatcher
(which is not linked to runTest
) to the Presenter
instanceLukasz Kalnik
02/07/2022, 6:24 PMTestScope
instance for both running the test and initializing the presenter.Joffrey
02/07/2022, 6:25 PMcoroutineContext
of the current test scope inside runTest
, so you should pass this one directly to the presenter without creating a new dispatcherLukasz Kalnik
02/07/2022, 6:28 PM@Test
fun `test flow`() = runTest {
val presenter = Presenter(coroutineContext, //...)
// ...
}
This indeed works. Thank you!Nick Allen
02/07/2022, 7:52 PMCoroutineScope
from an external CoroutineContext
seems a little odd. If the context has a Job
then that means the caller owns the lifetime, If it doesn't have a Job
then CoroutineScope
factory function creates a Job
and the Presenter
is in charge of managing the lifetime. I generally pass in CoroutineScope
and it's up to the caller to manage the lifetime or I pass in a specific context like CoroutineDispatcher
that definitely does not contain a Job
so the class can create it's own CoroutineScope
and be in change of cancelling it.Lukasz Kalnik
02/08/2022, 8:52 AMCoroutineScope
factory function itself. We have our own PresenterCoroutineScope
which creates a Job
and lets the Presenter
internally manage its own lifecycle.
But you are right, in this case we should only pass a dispatcher and not the whole context, including the externally created Job
.
Thank you for bringing this to my attention.Joffrey
02/08/2022, 8:55 AMcancel()
which would be unadvisable on a user-provided scopeLukasz Kalnik
02/08/2022, 8:56 AMPresenterCoroutineScope
a Dispatchers.Default
.Joffrey
02/08/2022, 8:57 AMLukasz Kalnik
02/08/2022, 8:57 AMrunTest
has to rethrow all exceptions that possibly happened in coroutines launched by Presenter
during the test in order for the test to fail.