What is the preferred way of unit testing classes ...
# coroutines
a
What is the preferred way of unit testing classes which internally launch a never-ending coroutine such as
SharedFlow
collection? Let’s say:
Copy code
class MyClass(coroutineScope: CoroutineScope, flowToCollect: Flow<Int>) {
    
    var lastObservedResult: Int? = null

    init {
        coroutineScope.launch {
            flowToCollect.collect { lastObservedResult = it }
        }
    }
}
If I use
runTest
and pass the created
TestScope
then the test is going to fail after some time because there is a running coroutine.
Copy code
@Test
fun testMyClass() = runTest {
    MyClass(this, flow)
    // do something here, make some assertions etc.
    // at the end, the test is going to fail because of the running coroutine
}
Copy code
After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs (...)
So should I create another scope instead? Like this, for example?
Copy code
val dispatcher = UnconfinedTestDispatcher()

@Test
fun testMyClass() = runTest(dispatcher) {
    val additionalScope = CoroutineScope(dispatcher)
    MyClass(additionalScope, flow)
    // do something here, make some assertions etc.

    additionalScope.close() // Is this necessary, btw? Is the launched coroutine going to leak after the test is finished or something?
    // now the test won't fail, but I must remember to close the additional scope manually
}
1
a
Yeah, I opened that after a while. I thought I was gonna find the answer here earlier than on GitHub and I didn’t want to spam my questions in the issues there 🙂 Thanks for linking 🙏
d
Typically, the community does answer the questions much faster (a couple of hours) than we do (from a couple of hours to several days, to several months, after elaborate internal discussions). It's more or less a coincidence that I happened to look at your issue quickly.
👍 1
a
I guess I was lucky today 🍀 🙂
m
Came here to ask exactly this question but with a little twist. If I `runTest(UnconfinedTestDispatcher()) {`and use the
runTest
scope to run the neverending coroutine, and then try to cancel the scope, the test fails with
Copy code
kotlinx.coroutines.JobCancellationException: TestScopeImpl was cancelled; job=TestScope[test ended]
E.g.
Copy code
runTest(UnconfinedTestDispatcher()) {
    val counter = Counter(this)
    ...
    this.cancel()
}
What works fine instead is
this.coroutineContext.cancelChildren()
. Is this the right thing to do? Also, my class has a
dispose
function which calls the
passedScope.cancel()
. I could use the
dispose()
function to be more explicit about what should be done, but the exception prevents that
158 Views