ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Member-only story

Eliminating Coroutine leaks in tests

Rick Busarow
ProAndroidDev
Published in
9 min readMay 24, 2019

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:

  1. before()
  2. create a leak()
  3. 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…

Create an account to read the full story.

The author made this story available to Medium members only.
If you’re new to Medium, create a new account to read this story on us.

Or, continue in mobile web

Already have an account? Sign in

Responses (3)

Write a response

Great Post! Helped me a lot to understand how Coroutines could leak in tests. 👍

1

Note that in order for Kotlin to understand the Rule, we need to add an @JvmField annotation to the property.

You can annotate your field as a getter and you won’t need the JvmField, i.e. @Rule:get.

15

Extensions instead have to inject properties to a class — through reflection, or through a little creative casting. We will utilize the second option since it’s cleaner and more perform...

JUnit 5.1 supports programmatic extensions that are closer to JUnit 4 Rules in the sense that are associated with an attribute on the test class, so you don’t need to inject it on each method.
See…