Reduce Boilerplate When Running Kotlin Tests
Uzi Landsmann
Systemutvecklare
Using a nifty Kotlin trick, you can keep your tests clean and easy to understand and maintain
Testing should be easy. If your tests are too complex and difficult to maintain, they lose meaning. Tests should help you understand your application logic and make sure that it does what it claims, so keeping your tests simple is a key point in making them useful for yourself and others who will have to deal with them later. One way to do so is to eliminate the boilerplate that might clutter your tests. This article will look at one way to achieve just that.
Scenario
Suppose you have an application that sends stuff to some receiving API. Here is the stuff we’re sending:
data class Stuff(val name: String, val type: String)
A service forwards the request to some other class for the actual sending. Here’s what the code looks like:
import okhttp3.OkHttpClient
class StuffService() {
private val client = OkHttpClient()
.newBuilder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Content-Type", "application/json")
.build()
chain.proceed(request)
}.build()
private val stuffLink = StuffLink(client, "http://some.where")
fun sendStuff(stuff: Stuff) = stuffLink.sendStuff(stuff)
}
The client (we call it a link to avoid having too many things called a client) does the actual sending by creating a request and using the given okhttp client and URL to send it. The code looks like this:
import com.fasterxml.jackson.databind.ObjectMapper
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
class StuffLink(private val client: OkHttpClient, private val url: String) {
private val objectMapper = ObjectMapper()
fun sendStuff(stuff: Stuff): Response {
val request = Request.Builder()
.url(url)
.post(
objectMapper.writeValueAsString(stuff)
.toRequestBody("application/json".toMediaType()),
).build()
return client.newCall(request).execute()
}
}
Test version 1
Now, suppose we want to test that the correct values are sent to the receiving API using the StuffLink class. Here’s our first attempt:
@Test
fun `Stuff should be sent by StuffLink (version 1)`() {
val requestSlot = slot<Request>()
val mockkClient = mockk<OkHttpClient> {
every { newCall(capture(requestSlot)) } returns mockk(relaxed = true)
}
val stuffLink = StuffLink(mockkClient, url)
val bob = Stuff("Bob", "Armchair")
stuffLink.sendStuff(bob)
val request = requestSlot.captured
val bodyAsMap = parseRequestBody(request)
assertThat(request.url.toString()).isEqualTo(url)
assertThat(bodyAsMap["name"]).isEqualTo("Bob")
assertThat(bodyAsMap["type"]).isEqualTo("Armchair")
}
About half of this method is boilerplate. We’re creating a slot as a placeholder for the request, a mock client instrumented with the slot, an instance of the StuffLink class to run the tests on, and then call the send method with the test data.
Only after this is done do we do what we intended: check the request contains what we expect it to.
Test version 2
Realizing that this is cumbersome, as we don’t want to repeat this practice in each test method we intend to write, we might try to put some of the preparation logic in a reusable method, as shown below:
@Test
fun `Stuff should be sent by StuffLink (version 2)`() {
val (mockkClient, requestSlot) = createMockAndSlot()
val stuffLink = StuffLink(mockkClient, url)
val bob = Stuff("Bob", "Armchair")
stuffLink.sendStuff(bob)
val request = requestSlot.captured
val bodyAsMap = parseRequestBody(request)
assertThat(request.url.toString()).isEqualTo(url)
assertThat(bodyAsMap["name"]).isEqualTo("Bob")
assertThat(bodyAsMap["type"]).isEqualTo("Armchair")
}
private fun createMockAndSlot(): Pair<OkHttpClient, CapturingSlot<Request>> {
val requestSlot = slot<Request>()
val mockkClient = mockk<OkHttpClient> {
every<Call> { newCall(capture(requestSlot)) } returns mockk<Call>(relaxed = true)
}
return mockkClient to requestSlot
}
Test version 3
Still, some boilerplate remains, and the reusable method is not too beautiful with its strange name and double return value. What we want is a way to avoid all of it. Here’s the third attempt:
@Test
fun `Stuff should be sent by StuffLink (version 3)`() {
val bob = Stuff("Bob", "Armchair")
val request = runInStuffLink { sendStuff(bob) }
val bodyAsMap = parseRequestBody(request)
assertThat(request.url.toString()).isEqualTo(url)
assertThat(bodyAsMap["name"]).isEqualTo("Bob")
assertThat(bodyAsMap["type"]).isEqualTo("Armchair")
}
private fun runInStuffLink(action: StuffLink.() -> Unit): Request {
val requestSlot = slot<Request>()
val mockkClient = mockk<OkHttpClient> {
every<Call> { newCall(capture(requestSlot)) } returns mockk<Call>(relaxed = true)
}
val stuffLink = StuffLink(mockkClient, url)
stuffLink.action()
return requestSlot.captured
}
The reusable runInStuffLink method lets us get rid of the rest of the boilerplate. It takes as an argument a method to run on the StuffLink class. The implementation creates the slot and the client, then creates an instance of the StuffLink class, and finally runs the given method on that instant and returns the captured slot.
What kind of black magic is this?
This technique above is explained in the Kotlin Function literals with receiver documentation. It is often used to design DSLs, as explained in detail in the Programming DSLs in Kotlin — by Venkat Subramaniam series, see for example the Design for Separate Implicit Contexts section.
The code for this article is available on this GitHub repository. Thank you for reading!