As all of us who write Java on a daily basis have experienced, we tend to spend a considerable amount of time writing boilerplate code. We can sometimes refactor that boilerplate away by using specialized classes for certain activities, but that doesn’t always bring us the right results. Reducing repetitive boilerplate can also be achieved by creating a DSL, a domain-specific language that can help us describe anything in a concise and targeted way whilst moving the actual work out of sight.
Kotlin has the tools on board to allow us to write type-safe DSL logic. In this post we’ll dive a bit deeper into how you could use them to your advantage. Here’s a quick preview of what we’ll be building:
A simple rest endpoint
Let’s start by defining a simple case; a Spring Boot based service containing an endpoint we can call to serve greetings. It could look a bit like this:
It is a really simple app, defined in the confines of one file — one of the luxuries Kotlin affords us. The app does little more than bootstrapping itself on a Tomcat server, and then exposes and endpoint at http://localhost:8080/api/hello/Jarno. Call it, and you will receive a nice greeting from the service, listed below.
"message": "Hi, Jarno!"
So far I am pretty pleased, this is a decent app with some solid functionality 😉 . We would want to test that it actually works though, so let’s write a simple test for it.
Testing the application
Spring Boot offers some solid functionality when it comes to testing your applications. In this case the goal is to do a black-box test for the application, just start it up on some random port and send a request to it to see if it responds in the way we would expect it to. We could define the test like so:
A couple of things are happening here. I’ll briefly cover them using the line(s) that they occur on:
- Line 1-2: Here we define the test. We are telling JUnit to use the
SpringRunnerand are telling the
SpringRunnerit is dealing with a Spring Boot application, which we want to be bootstrapped on a Tomcat instance using a random port (to prevent collisions).
- Line 5-6: Here we instruct Spring to inject a special implementation of the RestTemplate, which will have the port our application is running on preconfigured. If you would like to do this manually, you can always opt for injection of the port directly in your test classes:
@LocalServerPort lateinit var serverPort: Int; however it generally is more convenient to use the
- Line 8-20: This is where we define the actual test. We use the
TestRestTemplateto call the endpoint once the application has loaded and compare the output for the endpoint to see if it meets our requirements.
This is pretty straightforward, but what if we could define a way to do this that gives us more flexibility and helps us describe the test we are doing in a block of code that is easy to understand and reason about?
A DSL for testing REST calls
Why not try to define one here? In essence, to write tests as we did in the example shown above, we’ll define three core parts:
- The Contract – Defines the request-response contract we are testing;
- The Request – Defines what request to execute;
- The Response – Defines the expected response.
Let’s first have a look at the request. The core elements of a request (there are more, but we’ll be focusing on these for now):
- The request path –
- The request method –
- The request headers –
- The request body –
N/A -- (It's a GET).
Secondly, let’s take a look at the response we are expecting:
- The Response body – We expected a body, so we can check here if the server is responding according to our expectations;
- The Response status – Every response has a status code, which we can compare with any expected value.
To keep track of the properties I’ve listed above, We are going to define some data classes. Here’s what they might look like:
I’ve opted to use the builder pattern here. We can define defaults for some values where applicable, and use the
build() functions of the builder classes to do some validation if this is required. For instance, you could build in a check that requires the body to be non-null when the
Content-Type header is set, or vice-versa. We won’t cover the option in this example, but it is something to consider. The
Contract, Request and
Response have private constructors, so we can only create instances by going through the builders.
Now that we have these data classes, let’s define a DSL that can work with them:
This will not yet work. To make this work, we are going to use a functionality included in the Kotlin language, called lambda’s with receivers. We can define a lambda function that works on instances of a given type, meaning we can specify an expected type the lambda is supposed to operate on. In the lambda body we can then call functions on the provided instance and/or set some of its properties. Let’s define the
contract() function – which will be the entry point for our DSL:
Let’s look a little closer at the definition. Our
contract function takes a function as a parameter. It is defined as a function that can only be called on instances of
Contract.Builder, which returns ‘
Unit‘ (‘void’ in Java). The
contract function itself returns an instance of type
Contract, which can be inferred from the expression body but is explicitly declared here for clarity. To make this work, We are going to create a fresh new
Contract.Builder instance. Next, we are going to
apply(builder) (or call) the function that was passed as a parameter on this new instance. Finally, we are returning the result of the
Contract.Builder.build() function: a newly created
Of course, we also need to do this for the other elements in our DSL that define
headers. Here’s how we can do this:
These functions are defined to be slightly different. As you can see, the first two functions are declared as extension functions for the
Contract.Builder class – so they can only be applied to a
Contract.Builder instance. They both still use the lambda with receivers functionality, setting a property on the instance of
Contract.Builder we are defining in the DSL and then applying the contents of the lambda to them, just as we did in the
The third function adds a
headers function to the
Request.Builder class, allowing us to define headers. These functions, being extension functions of specific types make the DSL type safe which helps us in another area too: our IDE knows what types to use within the DSL and is able to provide content assist.
In the headers lambda, we can call functions on the headers just as we want to, so we can call any function that the
HttpHeaders instance publicly defines. We could add a header, like so:
put("Accept", listOf("application/json")), but that’s not very clean. We could also define another set of functions that add some of the most common headers to the request in a more convenient way:
As we saw earlier, these are extension functions, but this time added to the
HttpHeaders type, which means they can be called on any instance of this class. Now we have everything we need to define a fully specified
Contract instance, including the expectations we may have for it.
There’s one thing missing, though! We are not yet doing anything sensible with it. We are just creating an instance of the
Contract class. One last action to take is actually running the test and evaluating the results.
So, let’s add a function to the
Contract class, we’ll call it
verify. It will take a
TestRestTemplate as a parameter from the test and uses that to execute the request and verify the response. Here is the implementation:
In the implementation, We are using the configuration enclosed in the
Contract.request instance to set up the request (headers, body). We then execute the request, using the configured path and method configuration and assign it to a local value. After this, we evaluate the results by asserting the actual response against the values we configured in the
Contract.response. Here, we are using json-unit to compare the json snippets with each other. Of course, you can define several types of comparisons. Based on the accept header (
application/xml) you could use
xml-unit to compare any resulting bodies for validity and structure. We can put this all in the DSL implementation, out of sight for our DSL users.
Having added all this, we can now start writing tests like so:
This test should now run successfully. Please note that in the test we apply several Kotlin features, such as “raw strings”, a string delimited by a triple quote (
"""), which contains no escaping and can contain newlines and any other characters. This allows us to declare a body in a readable way — making it easy for us to edit it inline too. Of course, for larger responses this may not be convenient, but that’s a matter of taste. String interpolation also works for these raw strings, meaning you can use variables inside them too. Another feature we used is in the specification of the test function name. In Kotlin, you can define functions names between back-ticks, and when you do you can even use whitespace. This allows us to define test names that are easier to read.
Finally, if we were to change the request path, expected status, the expected body or the accept header for instance, we would expect to see test failures. Let’s try this with the path
/api/hello/Jim and leave everything else the same.
The test should fail and will show output similar to this:
java.lang.AssertionError: JSON documents are different:
Different value found in node "message", expected: <"Hi, Jarno!"> but was: <"Hi, Jim!">.
Expected :<"Hi, Jarno!">
Actual :<"Hi, Jim!">.
Looks like it is all working!
DSL-like structures have the potential to be useful for these kinds of repetitive activities. Building a simple DSL allows us to re-use logic and define tests in a clear and easy to read way – without a lot of excess noise. Additionally, you’ll get powerful IDE support with type-safety to boot. Here, we used it in a testing scenario, but you could just as easily use this in production code for calling external services.
In the project team I am currently a part of, we use this approach to do contract testing of our API’s.
The code for this example project is available in full on github.