Member-only story
Eliminating Coroutine leaks in tests

Coroutines are all the rage right now, and if you’re writing coroutines, you’re probably writing tests for them. Writing tests for coroutines can be difficult, not only because of concurrency concerns, but because it is very easy to create leaks which live on past the lifecycle of an individual test.
Consider this example class:
class Subject {
var someBoolean = false
fun CoroutineScope.loop() {
someBoolean = true
launch {
repeat(10) { count ->
delay(timeMillis = 1_000)
println("loop is running -- $count")
}
println("all done")
}
}
}
This single function loop()
will immediately update someBoolean
, then create a new coroutine with launch { }
. This coroutine will run asynchronously for 10 seconds, printing a reminder that it’s running every second. Since launch { }
is non-blocking, loop()
will return before the new coroutine finishes.
Now, let’s consider a simple JUnit 4 test class which may be testing that loop function.
import io.kotlintest.shouldBeclass MyLeakyTest {
val scope = CoroutineScope(Dispatchers.Unconfined)
val subject = Subject()
@Before
fun before() {
println("before my leaky test")
}
@After
fun after() {
println("after my leaky test")
}
@Test
fun `create a leak`() {
with(subject) {
scope.loop()
}
subject.someBoolean shouldBe true
println("my leaky test has completed")
}
}
If we run this test class, the order of execution will be:
before()
create a leak()
after()
Here’s the proof:
Everything seems to be fine, right? Well, that’s because we’re only executing this one test, and the entire JVM is shut down immediately after. But of course when we run tests, we want to run all of our tests. Let’s simulate that by adding one more test class:
class SomeOtherTest {
@Test
fun `another test`() {
println("some other tests would run now")
runBlocking { delay(11_000) }
}
}
Because this test uses runBlocking { }
, the delay it calls will ensure that the test doesn’t complete for 11 seconds. This…