fbpx

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.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
        }
    }
})
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.


more insights

Uncategorized
Jarosław Michalik

3 things that defined my Android dev career

Building an Android developer career isn’t just about coding—it’s about the experiences that push you to grow. Whether it’s connecting with other Android devs at meetups or mastering the fundamentals, it’s those key moments that shape how you approach the craft.

Read More »

AndroidPro newsletter 🚀

join 3057 developers reading AndroidPro newsletter every week

👉 tips & tricks, articles, videos

👉 every Thursday in your inbox

🎁 bonus on signup – Clean Architecture blueprint

brought to you by Jarek Michalik, Kotlin GDE

You can unsubscribe any time. For details visit our privacy policy.

android pro newsletter tips and tricks how to learn android