Ktor 1.6.4 Help

Testing

Ktor provides a special testing engine that doesn't create a web server, doesn't bind to sockets, and doesn't make any real HTTP requests. Instead, it hooks directly into internal mechanisms and processes an application call directly. This results in quicker tests execution compared to running a complete web server for testing. In addition, you can set up end-to-end tests for testing server endpoints using the Ktor HTTP client.

Add dependencies

To test a server Ktor application, you need to include the following artifacts in the build script:

  • Add the ktor-server-test-host dependency:

    testImplementation "io.ktor:ktor-server-test-host:$ktor_version"
    testImplementation("io.ktor:ktor-server-test-host:$ktor_version")
    <dependency> <groupId>io.ktor</groupId> <artifactId>ktor-server-test-host</artifactId> <version>${ktor_version}</version> </dependency>

  • Add the kotlin-test dependency providing a set of utility functions for performing assertions in tests:

    testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
    testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version")
    <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-test</artifactId> <version>${kotlin_version}</version> </dependency>

Testing overview

To use a testing engine, follow the steps below:

  1. Create a JUnit test class and a test function.

  2. Use withTestApplication function to set up a test environment for your application.

  3. Use the handleRequest function to send requests to your application and verify the results.

The code below demonstrates how to test the most simple Ktor application that accepts GET requests made to the / path and responds with a plain text response.

import io.ktor.application.* import io.ktor.http.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testRoot() { withTestApplication(Application::module) { handleRequest(HttpMethod.Get, "/").apply { assertEquals(HttpStatusCode.OK, response.status()) assertEquals("Hello, world!", response.content) } } } }
import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) fun Application.module(testing: Boolean = false) { routing { get("/") { call.respondText("Hello, world!") } } }

The runnable code example is available here: engine-main.

Send form data

To send form data in a test POST/PUT request, you need to set the Content-Type header and specify the request body. To do this, you can use the addHeader and setBody functions, respectively. The examples below show how to send form data using both x-www-form-urlencoded and multipart/form-data types.

x-www-form-urlencoded

A test below from the post-form-parameters example shows how to make a test request with form parameters sent using the x-www-form-urlencoded content type. Note that the formUrlEncode function is used to encode form parameters from a list of key/value pairs.

import io.ktor.application.* import io.ktor.http.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testRequests() = withTestApplication(Application::main) { with(handleRequest(HttpMethod.Post, "/signup"){ addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) setBody(listOf("username" to "JetBrains", "email" to "example@jetbrains.com", "password" to "foobar", "confirmation" to "foobar").formUrlEncode()) }) { assertEquals("The 'JetBrains' account is created", response.content) } } }
import io.ktor.application.* import io.ktor.html.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* import kotlinx.html.* fun Application.main() { routing { post("/signup") { val formParameters = call.receiveParameters() val username = formParameters["username"].toString() call.respondText("The '$username' account is created") } } }

multipart/form-data

The code below demonstrates how to build multipart/form-data and test file uploading. You can find the full example here: upload-file.

import io.ktor.application.* import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.testing.* import io.ktor.utils.io.streams.* import java.io.File import kotlin.test.* class ApplicationTest { @Test fun testRequests() = withTestApplication(Application::main) { with(handleRequest(HttpMethod.Post, "/upload"){ val boundary = "WebAppBoundary" val fileBytes = File("ktor_logo.png").readBytes() addHeader(HttpHeaders.ContentType, ContentType.MultiPart.FormData.withParameter("boundary", boundary).toString()) setBody(boundary, listOf( PartData.FormItem("Ktor logo", { }, headersOf( HttpHeaders.ContentDisposition, ContentDisposition.Inline .withParameter(ContentDisposition.Parameters.Name, "description") .toString() )), PartData.FileItem({ fileBytes.inputStream().asInput() }, {}, headersOf( HttpHeaders.ContentDisposition, ContentDisposition.File .withParameter(ContentDisposition.Parameters.Name, "image") .withParameter(ContentDisposition.Parameters.FileName, "ktor_logo.png") .toString() )) )) }) { assertEquals("Ktor logo is uploaded to 'uploads/ktor_logo.png'", response.content) } } }
import io.ktor.application.* import io.ktor.http.content.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* import java.io.File fun Application.main() { routing { var fileDescription = "" var fileName = "" post("/upload") { val multipartData = call.receiveMultipart() multipartData.forEachPart { part -> when (part) { is PartData.FormItem -> { fileDescription = part.value } is PartData.FileItem -> { fileName = part.originalFileName as String var fileBytes = part.streamProvider().readBytes() File("uploads/$fileName").writeBytes(fileBytes) } } } call.respondText("$fileDescription is uploaded to 'uploads/$fileName'") } } }

Preserve cookies during testing

If you need to preserve cookies between requests when testing, you can call handleRequest inside the cookiesSession function. In a test below from the session-cookie example, reload count is increased after each request since cookies are preserved.

import io.ktor.application.* import io.ktor.http.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testRequests() = withTestApplication(Application::main) { fun doRequestAndCheckResponse(path: String, expected: String) { handleRequest(HttpMethod.Get, path).apply { assertEquals(expected, response.content) } } cookiesSession { handleRequest(HttpMethod.Get, "/login") {}.apply {} doRequestAndCheckResponse("/", "Session ID is 123abc. Reload count is 0.") doRequestAndCheckResponse("/", "Session ID is 123abc. Reload count is 1.") doRequestAndCheckResponse("/", "Session ID is 123abc. Reload count is 2.") handleRequest(HttpMethod.Get, "/logout").apply {} doRequestAndCheckResponse("/", "Session doesn't exist or is expired.") } } }
import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* import io.ktor.sessions.* data class UserSession(val id: String, val count: Int) fun Application.main() { install(Sessions) { cookie<UserSession>("user_session") { cookie.path = "/" cookie.maxAgeInSeconds = 10 } } routing { get("/login") { call.sessions.set(UserSession(id = "123abc", count = 0)) call.respondRedirect("/") } get("/") { val userSession = call.sessions.get<UserSession>() if (userSession != null) { call.sessions.set(userSession.copy(count = userSession.count + 1)) call.respondText("Session ID is ${userSession.id}. Reload count is ${userSession.count}.") } else { call.respondText("Session doesn't exist or is expired.") } } get("/logout") { call.sessions.clear<UserSession>() call.respondRedirect("/") } } }

Define configuration properties in tests

If your application uses custom properties from the application.conf file, you also need to specify these properties for testing. This can be done in two ways:

  • Specify properties explicitly using MapApplicationConfig.

  • Load the existing application.conf and use it in a custom test environment.

MapApplicationConfig

The example below shows how to pass MapApplicationConfig to the withTestApplication function.

@Test fun testRequests() = withTestApplication() { (environment.config as MapApplicationConfig).apply { put("upload.dir", "uploads") } application.main() with(handleRequest(HttpMethod.Post, "/upload") { // Add headers/body }) { // Add assertions } }

You can find the full example for this approach here: UploadFileMapConfigTest.kt.

HoconApplicationConfig

The example below shows how to create a custom test environment with the configuration loaded from the existing application.conf file. Note that in this case the withApplication function is used to start a test engine instead of withTestApplication.

private val testEnv = createTestEnvironment { config = HoconApplicationConfig(ConfigFactory.load("application.conf")) } @Test fun testRequests() = withApplication(testEnv) { with(handleRequest(HttpMethod.Post, "/upload") { // Add headers/body }) { // Add assertions } }

You can find the full example for this approach here: UploadFileAppConfigTest.kt.

HttpsRedirect plugin

The HttpsRedirect plugin changes how testing is performed. Check the Testing section for more information.

Test with dependencies

In some cases, we will need some services and dependencies. Instead of storing them globally, we suggest you create a separate function receiving the service dependencies. This allows you to pass different (potentially mocked) dependencies in your tests.

class ApplicationTest { class ConstantRandom(val value: Int) : Random() { override fun next(bits: Int): Int = value } @Test fun testRequest() = withTestApplication({ testableModuleWithDependencies( random = ConstantRandom(7) ) }) { with(handleRequest(HttpMethod.Get, "/")) { assertEquals(HttpStatusCode.OK, response.status()) assertEquals("Random: 7", response.content) } with(handleRequest(HttpMethod.Get, "/index.html")) { assertFalse(requestHandled) } } }
fun Application.testableModule() { testableModuleWithDependencies( random = SecureRandom() ) } fun Application.testableModuleWithDependencies(random: Random) { routing { get("/") { call.respondText("Random: ${random.nextInt(100)}") } } }

End-to-end testing with HttpClient

Apart from a testing engine, you can use the Ktor HTTP Client for end-to-end testing of your server application.

import e2e.TestServer import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.client.statement.* import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test class EmbeddedServerTest: TestServer() { @Test fun rootRouteRespondsWithHelloWorldString(): Unit = runBlocking { val response: String = HttpClient().get<HttpResponse>("http://localhost:8080/").receive() assertEquals("Hello, world!", response) } }

For a full example, refer to a test of the embedded-server example.

Last modified: 10 August 2021