Writing a test DSL in Kotlin

As I previously wrote, I recently fell in love with the Kotlin language. It’s been over four months since that post, and my enthusiasm has not diminished. In this post, I’ll show how I combined some of Kotlin’s best features to write some extremely readable unit tests.

The thing we’ll be testing is my discrete physics engine on a 2D grid. Here’s an example:

In general, the situation consists of a number of objects and walls, each with a particular position and shape. Forces are applied to objects, and the physics engine is supposed to figure out how each object moves in response to those forces.

Structure of a test

Here’s how I can write a unit test for the above example:

@Test
fun exampleTest() = physicsTest {
  - "########"
  - "#  B D #"
  - "# ABCD #"
  - "#    D #"
  - "########"

  A isPushed right

  A shouldMove right
  B shouldMove right
  C shouldMove right
  D shouldMove right
}

Looks almost the same, right? And it looks almost like English, too. It reminds me a bit of the Gherkin language, but it’s all plain Kotlin.

  • The objects are described using a “grid” of strings, each introduced by a - character. Each letter identifies an object; cells with the same letter belong to the same object.

  • Forces are described with isPushed, e.g. A isPushed right.

  • Expected results are described with shouldMove, e.g. D shouldMove right.

I could have chosen to break it up by adding more blocks (withObjects { ... } andForces { ... } then { ... }), but I opted for brevity.

Let’s take this apart and see how it works.

First, the outer shell:

@Test
fun exampleTest() ...

This is just a JUnit test method, marked as usual with the @Test annotation. Nothing special about that.

But instead of a regular function body, we use an expression body, marked by the = symbol:

fun exampleTest() = physicsTest {
  ...
}

This is just for brevity. We could also have written it as a regular block body instead (note that no return is needed because the return type is Unit, i.e. nothing):

fun exampleTest() {
  physicsTest {
    ...
  }
}

The physicsTest function

Then, on to the secret sauce, the function physicsTest. It looks like this:

fun physicsTest(init: WorldBuilder.() -> Unit) =
    WorldBuilder().apply(init).check()

Whew, that’s a lot to unpack. Let’s take it step by step.

  • physicsTest is a function. It accepts one argument, init.

  • init is itself a function. Because it’s the last parameter in the list, Kotlin lets us take it outside the parenthesis when we call physicsTest, so we can write physicsTest { ... } as we saw above, instead of the clunky physicsTest({ ... }).

  • init has the type WorldBuilder.() -> Unit. So it’s a function, which takes no arguments (the () bit) and returns nothing (the -> Unit bit). What makes it special is the prefix WorldBuilder., which makes it a receiver function. This means the function must be called “in the scope of a WorldBuilder object”. In other words, it receives an implicit this argument which is a WorldBuilder, and all members of the WorldBuilder are in scope in the receiver function, just as if init were a regular member function of WorldBuilder. As we’ll see, this is very powerful.

  • The actual body of physicsTest first creates a WorldBuilder object by calling its constructor.

  • Then it calls .apply(init) on it. The apply function is in the Kotlin standard library; it means “run this receiver function on the object, and return the original object”. So now the init function is being run, operating on the WorldBuilder we just created.

  • Finally, .check() is being called on the WorldBuilder. This is a regular, honest, hard-working member function.

The function body could also be written thus:

fun physicsTest(init: WorldBuilder.() -> Unit) {
  val worldBuilder = WorldBuilder()
  worldBuilder.init()
  worldBuilder.check()
}

Note how we’re calling .init() just as if it were a member function of the WorldBuilder class, even though it’s actually a parameter being passed in to physicsTest. We can also write init(worldBuilder) if we prefer, which stresses the fact that init is not a regular member function of WorldBuilder, but hides the fact that it’s a receiver function.

I may have mentioned WorldBuilder a few times already. Let’s see what it looks like.

The WorldBuilder class

WorldBuilder is the class where all the nuts and bolts of the test infrastructure go.

class WorldBuilder {
  ...
}

I won’t describe the entire implementation in detail, but just point out some key member functions. First, let’s look at how objects are set up in the physics world; this bit:

  - "########"
  - "#  B D #"
  - "# ABCD #"
  - "#    D #"
  - "########"

How on earth is this “bulleted list” valid Kotlin syntax? The secret is operator overloading, in particular, overloading the unary minus. Normally, that’s the operator you use to turn a positive number into a negative: loss = -profit. But Kotlin lets us overload it on any type, not just on numbers.

(I’m not particularly fond of abusing operator overloading in this way, but all alternatives involve commas at the end of all lines but the last, which makes it less readable and more difficult to change.)

How does it work? As part of the WorldBuilder class, we have this:

  private val lines = mutableListOf<String>()

  operator fun String.unaryMinus() {
    lines.add(this)
  }

There are several things going on here.

  • The operator keyword marks the function as an overloaded operator.

  • The name unaryMinus determines which operator is being overloaded.

  • The part String. marks it as an extension function on the String class. This means it can be called as "...".unaryMinus(), even though it’s not actually part of the String class itself. And because it’s also an operator, it can be called as -"..." as well.

  • Whoa, you might think, you’re polluting the String class with this weird operator? No, fortunately not. Remember that this extension function is defined inside the WorldBuilder class, which restricts its scope. Writing -"..." anywhere else won’t work. But, crucially, it does work within the init function, because that’s a function with WorldBuilder as its receiver type.

  • Because the operator is defined inside WorldBuilder, it’s actually a member of WorldBuilder and can push the string onto the private lines list.

So really, it’s not magic, it’s just a combination of interesting Kotlin features that were designed to come together in just the right way to make this possible.

Next up, let’s see how these lines in the test work:

  A isPushed right

  A shouldMove right
  B shouldMove right
  C shouldMove right
  D shouldMove right

A through D and the directonal words left, right, up and down are just constants:

val A = 'A'
...
val D = 'D'
val left = Direction(-1, 0)
...
val down = Direction(0, 1)
val nowhere = Direction(0, 0)

The final constant, nowhere, lets me write A shouldMove nowhere.

The implementations of isPushed and shouldMove are similar, so let’s just look at isPushed:

  private val forces = mutableMapOf<Char, Direction>()

  infix fun Char.isPushed(to: Direction) {
    forces[this] = to
  }

After you’ve seen String.unaryMinus, this might be a bit easier to digest.

  • isPushed is an extension function on the Char type.

  • The infix keyword marks this as an infix function. Because of this, we don’t call A.isPushed(right), but rather A isPushed right.

The rest of it works in a similar way to String.unaryMinus: these infix functions are only in scope in members of the WorldBuilder class, and can access (private) members of that class.

The physicsTest function, revisited

So let’s now look back to where we came from:

fun physicsTest(init: WorldBuilder.() -> Unit) =
    WorldBuilder().apply(init).check()

A WorldBuilder is created, and the init function is called in its scope. This means that any invocations of String.unaryMinus, Char.isPushed and Char.shouldMove affect the WorldBuilder object.

So far, we’ve just been storing our objects and forces, without actually doing anything with them. This is why the final step is to call the .check() method on the WorldBuilder. The implementation of check is not relevant to this post; suffice it to say that it creates a physics world based on the stored-up specifications, simulates a time step, and asserts that the results are as they should be.

Conclusion

And there you have it: at the cost of about 100 lines of Kotlin code, I defined my own DSL (domain-specific language) that helps me add tests for this tricky physics problem extremely quickly. The designers of the Kotlin language did an amazing job of offering just the right features to make this possible. I’m excited to see where this already great language will go next!