fbpx

How to test a time-dependent coroutine

The issue

Unit testing is the fastest way to check if some piece of our application works as we expect. But what if we have to test some features that strongly depend on time? Should we allow our tests execute for instance for few minutes? Of course we shouldn’t! In this blog post I will show you two approaches to test kotlin coroutines code depending on time. Let’s start with the first example.

First example – countdown timer

Let’s assume we implemented timer using Kotlin Coroutines as below.

class Timer {

    fun start(durationInMillis: Long, intervalInMillis: Long): Flow<Long> = flow {
        for (elapsedTime in 0L..durationInMillis step intervalInMillis) {
            emit(elapsedTime)
            if (elapsedTime < durationInMillis) {
                delay(intervalInMillis)
            }
        }
    }
}

Before going any further, think for a moment how would you test this class.

I start with universal solution for such cases, applicable probably to any tool you are using for asynchronous programming. As you may noticed, the function have two parameters – duration and  interval. In real world scenario we probably set this timer for couple of minutes or seconds with one second interval. For test purpose we can easily decrease this values to milliseconds and thus reduce the tests execution time.

internal class TimerTest {

    private lateinit var timer: Timer

    private val validScenarios = mapOf(
        30L to 3L,
        1000L to 999L,
        1L to 1L,
        1890L to 37L
    )

    @BeforeEach
    fun setUp() {
        timer = Timer()
    }

    @Test
    fun `emits ticks of timer`() = runBlocking {
        validScenarios.forEach { (duration, interval) ->

            // All timer ticks are collected to a list
            val results: List<Long> = timer.start(duration, interval).toList()

            // Additional 'one' stands for a tick in time `zero`
            val expected: Int = (duration / interval).toInt() + 1

            assertEquals(
                expected = expected,
                actual = results.size
            )
        }
    }
}

All this is possible, because the timer is implemented according to inversion of control principle – the values like duration and interval are passed from outside the class. This allows to change the behaviour depending on use. You may also noticed the runBlocking function. It is needed here to execute coroutine code sequentially.

To be honest sometimes we don’t have such good conditions to write tests and this may not necessarily be the caused by sloppy code. Let’s see the second example.

Second example – countdown timer

Let’s imagine you have a task to add a push ups training feature to a fitness app. The entire exercise lasts 2 minutes. To simplify the example assume that this app is dedicated for extremely fit people and one full push-up takes 2 seconds – 1 seconds to raise the body and 1 seconds to lower it. Based on our timer example the implementation may looks like below.

sealed class PushUpState {
    object BodyUp : PushUpState()
    object BodyDown : PushUpState()
}
class PushUpTraining(
    private val timer: Timer,
) {
    fun start(): Flow<PushUpState> {
        return timer.start(
            durationInMillis = EXERCISE_DURATION,
            intervalInMillis = TIMER_INTERVAL,
        )
            .map { it.toInt() % 2 == 0 }
            .map { shouldBodyGoesDown -> 
                if (shouldBodyGoesDown) PushUpState.BodyDown 
                else PushUpState.BodyUp 
            }
    }

    internal companion object {
        const val EXERCISE_DURATION = 120000L
		const val TIMER_INTERVAL = 1000L
    }
}

Now we need to test this class somehow. As you can see, in this scenario duration and interval parameters are not passed from the outside of the class, but are declared inside. Does it mean we have to wait 2 minutes for test completion? Fortunately this won’t be nesesery if we use kotlin coroutines test library and advance the coroutine clock. How to achieve that? Let’s start with the library setup.

Setup

Add kotlinx-coroutine-test artifact to your project as test implementation.

dependencies {
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineTestVersion"
}

Are you looking for the latest library release? Go here.

Coroutine tests

In general to test a coroutine we need to execute it blocking. You saw it in previous example, where runBlocking function was used. In this case we can’t write tests the same way, because we want to also advance the clock by given time. The perfect fit for that is to use special coroutine builder, dedicated for that kind of problem, called runBlockingTest. The function launches all coroutines one by one (same as runBlocking) and also have ability to manipulate the coroutine clock. Keep in mind, that this is still experimental feature so should be annotated with @ExperimentalCoroutinesApi.

@ExperimentalCoroutinesApi
internal class PushUpTrainingTest {

    lateinit var timer: Timer
    lateinit var pushUpTraining: PushUpTraining

    @BeforeEach
    fun setUp() {
        // The timer is essential in this case, so the real object is created
        timer = Timer()
        pushUpTraining = PushUpTraining(timer)
    }

    @Test
    fun `makes 60 push ups during 2 minutes exercise`() = runBlockingTest {
        val expectedPushUps = 60

        // Set coroutine clock to exercise duration (2 minutes)
        advanceTimeBy(PushUpTraining.EXERCISE_DURATION)

        // Collect all training states (BodyUp & BodyDown) to a list     
        val results = pushUpTraining.start()
            .toList()

        // Dividing by two because two states represent a push up (BodyUp & BodyDown)
        val actualPushUps = results.size / 2

        assertEquals(
            expected = expectedPushUps,
            actual = actualPushUps,
        )
    }
}

We just write a test almost the same way as we do it usually. What is different? We add a pinch of magic by invoking advanceTimeBy function at the beginning of our test. The function simply advance the coroutine clock by given parameter, so test execution time can be reduced to milliseconds. Simple as that!

The above scenario is quite simple. What if we want to test the value from the middle of the exercise? In this case we need to add some improvements to our test.

@Test
fun `makes 30 push ups in 1 minute of the exercise`() = runBlockingTest {
    val exerciseDurationInMillis = 60000L
    val expectedPushUps = 30

    // Collect all training states (BodyUp & BodyDown) to a list
    val results = mutableListOf<PushUpState>()
    val job = launch {
        pushUpTraining.start()
            .collect { results.add(it) }
    }

    // Set coroutine clock to 1 minute
    advanceTimeBy(exerciseDurationInMillis)
    // Cancel coroutine to check result at specific time
    job.cancel()

    // Dividing by two because two states represent a push up (BodyUp & BodyDown)
    val actualPushUps = results.size / 2

    assertEquals(
        expected = expectedPushUps,
        actual = actualPushUps,
    )
}

The first thing is to collect data by adding it one by one to a list. Note also that the coroutine is wrapped with launch function to cancel its execution in specific time and check the result. You can use this test as a skeleton for looking into value within a particular time.

Summary

There are a several ways to test coroutines depending on the time. In this article I described two of them.

The first one assumes that the class or method under test is implemented according to inversion of control principle and the time parameter is passed from the outside. In this scenario time value can be easily changed for test purpose to reduce time of test execution.

The second one uses external kotlinx-coroutine-test library and takes advantage of manipulating coroutine clock. Even if there is no way to mock timer duration from the outside of the class, we can still invoke advanceTimeBy method to adjust the time to our needs.


more insights

Uncategorized
Jarosław Michalik

#kotlinDevChallenge 2023 summary

The KotlinDevChallenge was not just a competition, but a journey of learning, growth, and community engagement. Every participant in this challenge is a winner in

Read More »

AndroidPro newsletter 🚀

join 3057 developers reading AndroidPro newsletter every week

👉 tips & tricks, articles, videos

👉 every Thursday in your inbox

🎁 bonus on signup – Clean Architecture blueprint

brought to you by Jarek Michalik, Kotlin GDE

You can unsubscribe any time. For details visit our privacy policy.

android pro newsletter tips and tricks how to learn android