DSL for tests: after
In the first part of this story I explained a little bit what our project is about and how our tests used to look before Kotlin DSL was introduced.
DSL example
Let’s say we use StringBuilder
in our code really often. And at one point we realize that our pieces of code
look very similar. So we decide to extract this code to a function:
How do we use this function?
Writing builder
every time inside lambda’s body gets really annoying, so we decide to use Kotlin’s fancy feature -
functions with receiver.
This concept is related to extension functions. Sometimes you need to add a new method to a class that you can’t or don’t want to modify.
In Java you create a class called something like MyClassUtils
and add a public static
function there that receives
an instance of your class as its parameter. In Kotlin there is an easier way:
Notice that inside myNewFunction
body you can use MyClass
properties and methods
without explicitly calling them on some instance: function has implicit parameter of MyClass
type on which
methods are called. Inside extension function this
keyword can be omitted the same way as in regular methods.
Let’s now look closer at the type of buildAction
parameter: it’s a function that receives StringBuilder
object as its parameter and does something with it. We can replace it with function with receiver: it means that buildAction
is executed as if it’s an extension function of StringBuilder.
So our code now looks like this:
Btw, there is buildString
function
in Kotlin standard library.
After: great shiny DSL
Now when we understand how we can build DSL in Kotlin, let’s return to the educational plugin. Because I don’t want this post to be super long and boring, I’ll consider a simpler course structure (note that in reality we have many more layers):
First we need to understand how we want our DSL to look like. I’ll show you an example showing the whole course hierarchy, but we will implement only one layer because everything else is very similar.
So let’s finally implement course
function! First we need to figure out its type. What should it return?
It should return new Course object. That’s why we started the whole DSL story, right? What parameters should it take?
As we can see in our usage example it has two parameters. The first one is simple: it’s the name of a course. The second one
is lambda with receiver that should allow us to add new tasks inside.
To illustrate it with the code, that’s the signature of course
function:
What should happen inside this function? Well, we should create new instance, call buildCourse on it and then return it:
It works just fine, but we also need to declare task
function in a way that would allow us to have access to
Course
object inside it because we want to at least add new tasks to it.
We could define task
function as an extension function for Course
class. However, I don’t like this approach because
ideally this function should be available only inside DSL. So what can we do to fix it?
We can introduce a “helper” class that holds Course instance and serves as “receiver” in course
function:
For other layers (files, placeholder etc) we do the same.
For me at this point it already looks great, but I’d like to mention several improvements that we made:
- Introduced additional
courseWithFiles
function which generates files on file system for course and can be used on top level in DSL the same way ascourse
function. So we can use DSL in two situations now: when we want to do some checks with files on file system and when we don’t need physical files (in this case tests work much faster) - Introduced several functions to create different types of tasks as we have many (theory tasks, for example)
- Added support to mark answer placeholders directly in file text with tags:
Thank you for reading this story about how we introduced DSL in our tests!
Resources
- An article from official documentation which explains Kotlin features and syntax behind DSL
- Good article in two parts with a simple example Part 1 and Part 2
- Builders lesson (I especially recommend Html Builders task) from Kotlin Koans course in EduTools plugin. Instructions on how to install and use plugin can be found here
Leave a comment