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.Application
import 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.StringSpec
import io.kotest.matchers.shouldBe
import 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.StringSpec
import io.kotest.matchers.shouldBe
import 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
}
}
})
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.