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.
physicsTestis a function. It accepts one argument,init.initis itself a function. Because it’s the last parameter in the list, Kotlin lets us take it outside the parenthesis when we callphysicsTest, so we can writephysicsTest { ... }as we saw above, instead of the clunkyphysicsTest({ ... }).inithas the typeWorldBuilder.() -> Unit. So it’s a function, which takes no arguments (the()bit) and returns nothing (the-> Unitbit). What makes it special is the prefixWorldBuilder., which makes it a receiver function. This means the function must be called “in the scope of aWorldBuilderobject”. In other words, it receives an implicitthisargument which is aWorldBuilder, and all members of theWorldBuilderare in scope in the receiver function, just as ifinitwere a regular member function ofWorldBuilder. As we’ll see, this is very powerful.The actual body of
physicsTestfirst creates aWorldBuilderobject by calling its constructor.Then it calls
.apply(init)on it. Theapplyfunction is in the Kotlin standard library; it means “run this receiver function on the object, and return the original object”. So now theinitfunction is being run, operating on theWorldBuilderwe just created.Finally,
.check()is being called on theWorldBuilder. 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
operatorkeyword marks the function as an overloaded operator.The name
unaryMinusdetermines which operator is being overloaded.The part
String.marks it as an extension function on theStringclass. This means it can be called as"...".unaryMinus(), even though it’s not actually part of theStringclass itself. And because it’s also an operator, it can be called as-"..."as well.Whoa, you might think, you’re polluting the
Stringclass with this weird operator? No, fortunately not. Remember that this extension function is defined inside theWorldBuilderclass, which restricts its scope. Writing-"..."anywhere else won’t work. But, crucially, it does work within theinitfunction, because that’s a function withWorldBuilderas its receiver type.Because the operator is defined inside
WorldBuilder, it’s actually a member ofWorldBuilderand can push the string onto the privatelineslist.
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.
isPushedis an extension function on theChartype.The
infixkeyword marks this as an infix function. Because of this, we don’t callA.isPushed(right), but ratherA 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!