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 callphysicsTest
, so we can writephysicsTest { ... }
as we saw above, instead of the clunkyphysicsTest({ ... })
.init
has the typeWorldBuilder.() -> Unit
. So it’s a function, which takes no arguments (the()
bit) and returns nothing (the-> Unit
bit). What makes it special is the prefixWorldBuilder.
, which makes it a receiver function. This means the function must be called “in the scope of aWorldBuilder
object”. In other words, it receives an implicitthis
argument which is aWorldBuilder
, and all members of theWorldBuilder
are in scope in the receiver function, just as ifinit
were a regular member function ofWorldBuilder
. As we’ll see, this is very powerful.The actual body of
physicsTest
first creates aWorldBuilder
object by calling its constructor.Then it calls
.apply(init)
on it. Theapply
function is in the Kotlin standard library; it means “run this receiver function on the object, and return the original object”. So now theinit
function is being run, operating on theWorldBuilder
we 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
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 theString
class. This means it can be called as"...".unaryMinus()
, even though it’s not actually part of theString
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 theWorldBuilder
class, which restricts its scope. Writing-"..."
anywhere else won’t work. But, crucially, it does work within theinit
function, because that’s a function withWorldBuilder
as its receiver type.Because the operator is defined inside
WorldBuilder
, it’s actually a member ofWorldBuilder
and can push the string onto the privatelines
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 theChar
type.The
infix
keyword 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!