Intro to testing Ktor controllers

Let’s suppose that we want to dismiss all calls for api/restricted endpoint if they don’t have special header, in this case „X-Special-Header”. We access header via get PipelineContext  and check if that value is null or blank. If so, we respond with HTTP 403. If given header is present,  we proceed with some business logic and respond with HTTP 200.

Usually that kind of mechanisms are implemented in application-wide request interceptors, so there is no need to check header presence in controller method

System under test implementation

The very basic implementation looks like that:

import io.ktor.application.*import io.ktor.application.Applicationimport io.ktor.http.*import io.ktor.request.*import io.ktor.response.*import io.ktor.routing.*fun Application.restrictedApiModule() {    routing {        get("/api/restricted") {            val authorizationHeader = context.request.header("X-Special-Header")            if (authorizationHeader.isNullOrBlank()) {                call.respond(HttpStatusCode.Forbidden)            } else {                //do some business logic                call.respond(HttpStatusCode.OK)            }        }    }}
System under test – restricted api module

How we would like to approach writing tests for that?

Test case

Let’s start with writing test method declaration:

class RestrictedApiTest : StringSpec({    "given X-Special-Header header is present when handle request then respond OK"{            }}

We will make use of Ktor library for testing: ktor-server-test-host (available as Gradle dependency)

testImplementation "io.ktor:ktor-server-test-host:1.4.0"
package io.ktor.server.testing/** * Start test application engine, pass it to [test] function and stop it */fun <R> withTestApplication(moduleFunction: Application.() -> Unit, test: TestApplicationEngine.() -> R): R {    return withApplication(createTestEnvironment()) {        moduleFunction(application)        test()    }}

Function withTestApplication accepts function on Application – we can pass our restrictedApiModule in the following way:

class RestrictedApiTest : StringSpec({    "given X-Special-Header header is present when handle request then respond OK"{        withTestApplication(moduleFunction = { restrictedApiModule() }) {        }    }})

And make test call handleRequest() method:

class RestrictedApiTest : StringSpec({    "given X-Special-Header header is present when handle request then respond OK"{        withTestApplication(moduleFunction = { restrictedApiModule() }) {            val testCall: TestApplicationCall = handleRequest(method = HttpMethod.Get, uri = "/api/restricted") {                addHeader("X-Special-Header", "some-header-value")            }        }    }})

We define HTTP method which should be executed, and additionally add given header. Then, we can perform assertion on TestApplicationCall – in this case check if response status is actually HTTP 200 – OK.

import io.kotest.core.spec.style.StringSpecimport io.kotest.matchers.shouldBeimport io.ktor.http.*import io.ktor.server.testing.*class RestrictedApiTest : StringSpec({    "given X-Special-Header header is present when handle request then respond OK"{        withTestApplication(moduleFunction = { restrictedApiModule() }) {            val testCall: TestApplicationCall = handleRequest(method = HttpMethod.Get, uri = "/api/restricted") {                addHeader("X-Special-Header", "some-header-value")            }            testCall.response.status() shouldBe HttpStatusCode.OK        }    }})
Test case – check if HTTP 200 was returned when X-Special-Header was present in request

More test cases

We also should check other test case – if there was HTTP 403 – Forbidden returned when our header is blank or null.

import io.kotest.core.spec.style.StringSpecimport io.kotest.matchers.shouldBeimport io.ktor.http.*import io.ktor.server.testing.*class RestrictedApiTest : StringSpec({    "given X-Special-Header header is not present when handle request then respond Forbidden"{        withTestApplication(moduleFunction = { restrictedApiModule() }) {            val testCall: TestApplicationCall = handleRequest(method = HttpMethod.Get, uri = "/api/restricted"){                addHeader("X-Special-Header", "")            }            testCall.response.status() shouldBe HttpStatusCode.Forbidden        }    }    "given X-Special-Header header is present when handle request then respond OK"{        withTestApplication(moduleFunction = { restrictedApiModule() }) {            val testCall: TestApplicationCall = handleRequest(method = HttpMethod.Get, uri = "/api/restricted") {                addHeader("X-Special-Header", "some-header-value")            }            testCall.response.status() shouldBe HttpStatusCode.OK        }    }})

Test results, as rendered in IntelliJ

Summary

The whole Ktor server environment feels lightweight and flexible – as opposed to Spring (which by the way has Kotlin support).

With tooling like that it’s just doesn’t feel right to omit functional tests in application.

Leave a Comment