https://kotlinlang.org logo
Title
m

mboudraa

02/21/2022, 3:15 PM
Hey folks, I'm trying to write a test to make sure coroutines are actually cancelled. But I keep failing at it. So I'm actually not sure I understand what I'm doing. Here's my use case: I have a
Store
which is essentially a wrapper on top of 2 flows. From this store I can dispatch events and those events will trigger coroutines to be executed on a given scope. However when I cancel the scope, the coroutines aren't cancelled...
suspend fun dispatch(action: Action) {
        val currentState = stateFlow.value

        _actionFlow.tryEmit(action)

        supervisorScope {
            sideEffects.forEach { sideEffect ->
                launch {
                    sideEffect(currentState, action)?.let { dispatch(it) }
                }
            }
        }
}
Here's how my test is written
@Test
    fun should_cancel_side_effect() = runTest {
        launch {
            val store = createStore(this) {
                registerSideEffect sideEffect { _, action ->
                    if (action !is TestAction.Add) return@sideEffect null
                    delay(1_000)
                    return@sideEffect TestAction.Remove(3)
                }
            }

            store.stateFlow.test {
                CoroutineScope(Job()).launch childScope@{
                    store.dispatch(TestAction.Add(3))
                    cancel()
                }.join()

                assertEquals(TestCounterState(0), awaitItem())

                this@launch.cancel()
            }
        }.join()
    }
But the side effect returns the
TestAction.Remove(3)
like I never called
cancel()
on the scope that launched the coroutine
n

Nick Allen

02/21/2022, 8:16 PM
1. In the future, please put long code snippets in thread to keep the channel easier to read. 2. Seems like there were some errors transcribing you code.
registerSideEffect sideEffect { _, action ->
? Is it
dispatch
or
suspendDispatch
? Does
dispatch
launch a coroutine that calls
suspendDispatch
? Please ensure code samples have sufficient info for others to understand what's going on. Linking to examples on https://play.kotlinlang.org can help a lot. 3.
Job()
does not create a child job. It creates a
Job
with no parent. I suspect this or similar issue is the root of your misunderstanding. 4. I can't think of a reason you'd ever want to call
CoroutineScope(...).launch {...}
.
CoroutineScope
function is generally used to create a
CoroutineScope
that is saved to a member property and represents the lifetime of the instance and is cancelled when the object is closed/cancelled so all work launched from that
CoroutineScope
can be cancelled.
m

mboudraa

02/21/2022, 8:51 PM
1. Noted. Will do next time 🙂 2.
suspendDispatch
and
dispatch
are the same thing it's a typo that remained while I was trying stuff. (FIxed) 3. 4. I'm trying to simulate that the coroutine is launched from another parent with a very different scope
n

Nick Allen

02/21/2022, 10:35 PM
Are you aware that
supervisorScope
waits for child jobs to finish (as any suspend method should)? So the side effects are all completely done by the time
dispatch
returns. If you want
dispatch
to start work, but not finish, then it should not suspend and instead take a
CoroutineScope
as a parameter in order to
launch/async
the work.
m

mboudraa

02/21/2022, 11:29 PM
Oh thanks Nick. TIL
g

gildor

02/22/2022, 4:15 AM
I’m trying to simulate that the coroutine is launched from another parent
It doesn’t look as the most clean way to do this, keep all your coroutines childs of parent job, and just launch new coroutine using standard launch (it creates own scope with child job)