Testing Functional Kotlin

Dependency injection is a pattern very commonly used in Object-Oriented Java. It is sometimes done using frameworks such as Dagger, Spring, or Guice, and sometimes it is as simple as making sure all dependencies of a class are provided through the constructor. One of the major benefits of dependency injection is that it gives you the ability to isolate a class for testing by injecting mocks. For example:

class FizzBuzzRunner( val linePrinter: PrintStream = System.out) {

  fun fizzBuzz(limit: Int) { 
    for(i in 1..limit) { 
      linePrinter.println(calculateValueForNumber(i))
    }
  }

  fun calculateValueForNumber(number: Int) { 
    val sb = StringBuilder()
    if (number % 3 == 0) {  sb.append("FIZZ") }
    if (number % 5 == 0) {  sb.append("BUZZ") }
    if (sb.isEmpty()) {  sb.append(number) }
    return sb.toString()
  }
}

FizzBuzzRunner will print to System.out in production, but we can inject a mock PrintStream for testing.

If you are like most Kotlin developers, you come from an Object-Oriented Java background. You most likely start out by writing Object-Oriented Kotlin that looks a lot like Java. As you became more comfortable, you might start taking advantage of Kotlin’s ability to be functional. This means writing first-class functions, not classes. Functional programming has led me to rethink my approaches to software design, for the better. However, I soon realized that as great as pure functions are, at some point you have to compose them into higher level program functions, or some kind of procedural flow. Functions will call other functions. When one function calls another, how can you test the caller function?

Our FizzBuzz code will now look like this:


fun fizzBuzz(limit: Int) { 
  for(i in 1..limit) { 
    print(calculateValueForNumber(i))
  }
}


fun calculateValueForNumber(number: Int) { 
  val sb = StringBuilder()
  if (number % 3 == 0) {  sb.append("FIZZ") }
  if (number % 5 == 0) {  sb.append("BUZZ") }
  if (sb.isEmpty()) {  sb.append(number) }
  return sb.toString()
}


fun print(string: String) { 
  System.out.println(string)
}

In this case, we have no way to mock out the actual printing function. One approach is to just let that function make it’s calls, and test it as a component-style test. But when it is calling something at the system boundary, like a database call, or, in this case, I/O, that will not work so well. So I took another approach. In addition to my function’s inputs, it will also take functional arguments for any other functions it needs to invoke. These parameters will have a default value of a static method reference to the function it will call, so that my production code looks the same, and these dependencies will not need to be passed all the way down from my main method. However, for testing, I can pass in a lambda for that parameter as a mock, and in this way, I can isolate my function. This approach is known as "Ad-hoc Polymorphism". The fizzBuzz function now looks like this:


fun fizzBuzz(limit: Int, printer: (String) -> Unit = ::print) { 
  for(i in 1..limit) { 
    printer.invoke(calculateValueForNumber(i))
  }
}

We can invoke it in production code just the same:

fizzBuzz(4)

And it can be isolated for testing like this:


fun test() { 
  val outputList = ArrayList<String>()
  fizzBuzz(6, {  outputList.add(it) })
  assertThat(outputList[2]).isEqualTo("FIZZ")
}

The beauty of this approach is that it can also be used in your object-oriented “edge” code to inject pure “core” functions into your objects which call them. For example:

class FizzBuzzRunner(
    private val printer: (String) -> Unit = ::print
    private val calculator: (Int) -> String = ::calculateForNumber) {

  fun fizzBuzz(limit: Int) { 
    for(i in 1..limit) { 
      printer.invoke(calculator.invoke(i))
    }
  }

  fun print(string: String) { 
    System.out.println(string)
  }

  fun calculateValueForNumber(number: Int) { 
    val sb = StringBuilder()
    if (number % 3 == 0) {  sb.append("FIZZ") }
    if (number % 5 == 0) {  sb.append("BUZZ") }
    if (sb.isEmpty()) {  sb.append(number) }
    return sb.toString()
  }
}

This is a simple pattern I have found to write more testable functional Kotlin, and I hope it is useful to others.

Discuss this post on Mastodon or Twitter.