Post

Make Kotlin code testable with interfaces

Before you begin…

Please pull up an IDE with:

  • one file for production code and
  • one file for JUnit tests

1. Testing the time

Let’s say we have a class that fetches the current time and does something with it:

1
2
3
4
5
6
class Greeter {
    fun greet(): String {
        val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) // hardcoded
        return if (hour < 12) "Good morning" else "Good evening"
    }
}

We want to test the case where greet returns “Good morning”, AND the case where greet returns “Good evening”. The problem is that Calendar.getInstance() fetches the real system time.

Consider the below example; this unit test will awkwardly fail for half the day!

1
2
3
4
5
@Test
fun testMorningGreeting() {
    val greeter = Greeter()
    assertEquals("Good morning", greeter.greet())
}

Here’s a thought - greet doesn’t actually care where you get the time from. It just wants something that can get the time.

When you get that thought, that’s when you need an interface!

1
2
3
interface Clock {
    fun currentHour(): Int
}

The interface Clock says, “a Clock is anything that gives you the currentHour”. As long as it’s a Clock, it should be usable by Greeter:

1
2
3
4
5
6
class Greeter(private val clock: Clock) { // just give me a Clock, any Clock!
    fun greet(): String {
        val hour = clock.currentHour()
        return if (hour < 12) "Good morning" else "Good evening"
    }
}

You want something, anything that implements Clock. Here’s two examples:

1
2
3
4
5
6
7
8
9
class SystemClock : Clock {
    override fun currentHour(): Int {
        return Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
    }
}

class HardcodedClock(private val fixedHour: Int) : Clock {
    override fun currentHour(): Int = fixedHour
}

The SystemClock is taken from the first code snippet; you would use this for your actual code. On the other hand, the HardcodedClock is really useful for unit tests:

1
2
3
4
5
6
7
8
9
10
11
@Test
fun testMorningGreeting() {
    val greeter = Greeter(HardcodedClock(9))
    assertEquals("Good morning", greeter.greet())
}

@Test
fun testEveningGreeting() {
    val greeter = Greeter(HardcodedClock(18))
    assertEquals("Good evening", greeter.greet())
}

By swapping out the Clock implementation, we’re able to write consistent, useful unit tests for all scenarios!

2. Testing a network call

Network calls can either succeed or fail. You probably have code paths for both cases. Just like how you shouldn’t use a real system clock during unit tests, you shouldn’t make real network calls either.

Say you have a WeatherForecaster that tells you if it is hot today:

1
2
3
4
5
6
7
8
9
10
11
12
class WeatherForecaster() {
    suspend fun isHotToday(): Boolean {
        return try {
            // CnnWeatherForecast is a class that makes a network request to cnn.com
            val weatherApi = CnnWeatherForecast()
            val weather = weatherApi.getCurrentWeather()
            weather.temperatureCelsius > 30.0
        } catch (e: Exception) {
            false
        }
    }
}

Like the last example, we can’t control if cnn.com will give us a temperature above or below 30C. We also can’t control if cnn.com will give us a network error or not.

Once again, we just want something that can get us the weather. We’ll take anything that satisfies the interface WeatherApi:

1
2
3
4
5
6
7
8
9
10
11
12
13
data class WeatherResponse(val temperatureCelsius: Double)

interface WeatherApi {
    suspend fun getCurrentWeather(): WeatherResponse
}

// we'll take any WeatherApi implementation, NOT just cnn.com!
class WeatherForecaster(private val weatherApi: WeatherApi) {
    suspend fun isHotToday(): Boolean {
        val weather = weatherApi.getCurrentWeather()
        return weather.temperatureCelsius > 30.0
    }
}

In the last example, we literally typed up a HardcodedClock : Clock that we could manipulate for our desired output. But what if I told you there were libraries out there that do this for you?

I’ll use the mockk library this time - mockk<WeatherApi>() automagically creates an object that implements WeatherApi. On top of that, we’ll get helper test functions that verify we called the weatherApi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@ExperimentalCoroutinesApi
class WeatherForecasterTest {

    private val weatherApi = mockk<WeatherApi>()
    private lateinit var forecaster: WeatherForecaster

    @Before
    fun setup() {
        forecaster = WeatherForecaster(weatherApi)
    }

    @Test
    fun `isHotToday returns true when temperature is above 30`() = runTest {
        val hotWeather = WeatherResponse(temperatureCelsius = 35.0)
        coEvery { weatherApi.getCurrentWeather() } returns hotWeather

        val result = forecaster.isHotToday()

        assertTrue(result)
        coVerify(exactly = 1) { weatherApi.getCurrentWeather() }
    }

    @Test
    fun `isHotToday returns false when temperature is below or equal to 30`() = runTest {
        val coolWeather = WeatherResponse(temperatureCelsius = 25.0)
        coEvery { weatherApi.getCurrentWeather() } returns coolWeather

        val result = forecaster.isHotToday()

        assertFalse(result)
        coVerify(exactly = 1) { weatherApi.getCurrentWeather() }
    }

    @Test
    fun `isHotToday returns false when API throws exception`() = runTest {
        coEvery { weatherApi.getCurrentWeather() } throws IOException("No network")

        val result = forecaster.isHotToday()

        assertFalse(result)
        coVerify(exactly = 1) { weatherApi.getCurrentWeather() }
    }
}

Ok, but what if my code uses Spring, or X/Y/Z framework

That’s totally fine! From the last two examples, you’ve learned that your functions/classes should accept interfaces. If your code feels untestable, there’s probably a part in there that you could separate with an interface. It should be pretty obvious - usually you’re interfacing out a clock, a network request, or a database call.

By the way, you just learned dependency injection

Dependency injection is literally just when you go from this:

1
2
3
4
5
6
class Greeter {
    fun greet(): String {
        val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) // hardcoded
        return if (hour < 12) "Good morning" else "Good evening"
    }
}

to this:

1
2
3
4
5
6
7
8
9
10
class Greeter(private val clock: Clock) { // just give me a Clock, any Clock!
    fun greet(): String {
        val hour = clock.currentHour()
        return if (hour < 12) "Good morning" else "Good evening"
    }
}

interface Clock {
    fun currentHour(): Int
}

Author’s note

I struggled with understanding testable code for the longest time. It feels like the popular resources today, although well-intentioned, just don’t explain this topic intuitively. As readers, it’s too easy to nod our heads to flowery buzzwords. I hope after reading this article, you understand that testable code isn’t just some moral highground - it’s also just easier to work with.

This post is licensed under CC BY 4.0 by the author.