Ktor 3.4.0 Help

Testing in Ktor Server

Ktor provides a testing engine that runs application calls directly without starting a real web server or binding to sockets. Requests are processed internally, which makes tests faster and more reliable compared to running a full server.

Add dependencies

To test a Ktor server application, include the following dependencies in your build script:

  • The ktor-server-test-host dependency provides the testing engine:

    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-jvm</artifactId> <version>${ktor_version}</version> </dependency>
  • The kotlin-test dependency provides a set of utility functions for performing assertions:

    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

You can test a Ktor application using the testApplication {} function and the provided HTTP client. A typical workflow includes the following steps:

  1. Define a test using testApplication {}.

  2. Configure and run a test instance of your application.

  3. Optionally, configure the HTTP client.

  4. Use the client to make HTTP requests to your test application and receive responses.

  5. Verify responses using assertions from kotlin.test, including status codes, headers, and body content.

The following example tests a simple Ktor application that responds to GET / requests with plain text:

package com.example import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { application { module() } val response = client.get("/") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello, world!", response.bodyAsText()) } }
package com.example import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) fun Application.module() { routing { get("/") { call.respondText("Hello, world!") } } }

Set up a JUnit test class

Before writing tests for your Ktor application, create a test file and a JUnit test class.

  1. Locate or create the src/test/kotlin directory in your project.

  2. Create a new Kotlin file (for example, ApplicationTest.kt).

  3. Define a Kotlin class that will contain your tests:

    class ApplicationTest { // Test functions go here }
  4. Add a test function annotated with @Test. Inside the test, use the testApplication {} function to run your application in a test environment:

    class ApplicationTest { @Test fun testRoot() = testApplication { // ... } }

    The testApplication {} function is the entry point for server testing in Ktor. It creates an isolated test environment, runs your application without starting a real web server, and provides a preconfigured HTTP client for making requests and asserting responses.

    Inside the testApplication {} block, you configure how the test application should behave, such as which modules to load, which routes to expose, how the environment is set up, or which external services are mocked.

    The following section describes the available configuration options.

    Configure a test application

    When configuring a test application, you can:

    Add application modules

    Modules must be loaded to a test application either by explicitly loading them or by configuring the environment.

    Explicit module loading

    To add modules to a test application manually, use the application {} block:

    @Test fun testModule1() = testApplication { application { module1() module2() } val response = client.get("/module1") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello from 'module1'!", response.bodyAsText()) }

    Load modules from a configuration file

    To load modules from a configuration file, use the environment {} block to specify the configuration file for your test:

    @Test fun testHello() = testApplication { environment { config = ApplicationConfig("application-custom.conf") } }

    This method is useful when you need to mimic different environments or use custom configuration settings during testing.

    Access the application instance

    Inside the application {} block, you can access the Application instance being configured:

    testApplication { application { val app: Application = this // Interact with the application instance here } }

    Additionally, the testApplication scope exposes the application property, which returns the same Application instance used by the test. This allows you to inspect or interact with the application directly from your test code.

    @Test fun testAccessApplicationInstance() = testApplication { lateinit var configuredApplication: Application application { configuredApplication = this } startApplication() // Accesses the application property val app: Application = application // Asserts it’s the same instance assertSame(configuredApplication, app) }

    Add routes

    You can add routes to your test application using the routing {} block. This approach is useful for testing routes without loading full modules or for adding test-specific endpoints.

    The following example adds the /login-test endpoint used to initialize a user session in tests:

    fun testHello() = testApplication { routing { get("/login-test") { call.sessions.set(UserSession("xyzABC123","abc123")) } } }

    Customize the environment

    To configure a custom environment for your test application, use the environment {} function.

    For example, to load a configuration file from the test/resources folder:

    @Test fun testHello() = testApplication { environment { config = ApplicationConfig("application-custom.conf") } }

    Alternatively, you can provide configuration properties programmatically using MapApplicationConfig. This is useful when you need access to application configuration before the application starts.

    @Test fun testDevEnvironment() = testApplication { environment { config = MapApplicationConfig("ktor.environment" to "dev") } }

    Mock external services

    You can simulate external services using the externalServices {} function. Inside its block, use the hosts() {} function for each service you want to mock. Within the hosts() {} block, you can configure an Application that acts as the mock service by defining routes and installing plugins.

    The following example simulates a JSON response from a Google API:

    @Test fun testHello() = testApplication { externalServices { hosts("https://www.googleapis.com") { install(io.ktor.server.plugins.contentnegotiation.ContentNegotiation) { json() } routing { get("oauth2/v2/userinfo") { call.respond(UserInfo("1", "JetBrains", "", "")) } } } } }

    Configure a client

    The testApplication {} function provides a configured HTTP client through the client property. To customize the client and install additional plugins, use the createClient {} function.

    For example, you can install the ContentNegotiation plugin to send JSON data in a POST/PUT request:

    @Test fun testPostCustomer() = testApplication { application { main() } client = createClient { install(ContentNegotiation) { json() } } }

    Make a request

    Use the configured client to make requests and receive responses.

    The following example tests the /customer endpoint that handles POST requests:

    @Test fun testPostCustomer() = testApplication { application { main() } client = createClient { install(ContentNegotiation) { json() } } val response = client.post("/customer") { contentType(ContentType.Application.Json) setBody(Customer(3, "Jet", "Brains")) } }

    Assert results

    After receiving a response, you can verify the results using assertions from the kotlin.test library:

    @Test fun testPostCustomer() = testApplication { application { main() } client = createClient { install(ContentNegotiation) { json() } } val response = client.post("/customer") { contentType(ContentType.Application.Json) setBody(Customer(3, "Jet", "Brains")) } assertEquals("Customer stored correctly", response.bodyAsText()) assertEquals(HttpStatusCode.Created, response.status) }

    Test POST/PUT requests

    Send form data

    To send form data in a test request, set the Content-Type header and the request body using the header() and setBody() functions.

    Key/value pairs

    To send key/value form parameters in a POST request, set the Content-Type header to application/x-www-form-urlencoded and encode the parameters using the formUrlEncode() function:

    package formparameters import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testPost() = testApplication { application { main() } val response = client.post("/signup") { header(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.bodyAsText()) } }
    import io.ktor.server.application.* import io.ktor.server.html.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.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

    You can use the multipart/form-data content type to build multipart form data and test file uploads:

    package uploadfile import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import org.junit.* import java.io.* import kotlin.test.* import kotlin.test.Test class ApplicationTest { @Test fun testUpload() = testApplication { application { main() } val boundary = "WebAppBoundary" val response = client.post("/upload") { setBody( MultiPartFormDataContent( formData { append("description", "Ktor logo") append("image", File("ktor_logo.png").readBytes().toString(), Headers.build { append(HttpHeaders.ContentType, "image/png") append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"") }) }, boundary, ContentType.MultiPart.FormData.withParameter("boundary", boundary) ) ) } assertEquals("Ktor logo is uploaded to 'uploads/ktor_logo.png'", response.bodyAsText(Charsets.UTF_8)) } @After fun deleteUploadedFile() { File("uploads/ktor_logo.png").delete() } }
    package uploadfile import io.ktor.server.application.* import io.ktor.http.content.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.util.cio.* import io.ktor.utils.io.* import java.io.File fun Application.main() { routing { post("/upload") { var fileDescription = "" var fileName = "" val multipartData = call.receiveMultipart(formFieldLimit = 1024 * 1024 * 100) multipartData.forEachPart { part -> when (part) { is PartData.FormItem -> { fileDescription = part.value } is PartData.FileItem -> { fileName = part.originalFileName as String val file = File("uploads/$fileName") part.provider().copyAndClose(file.writeChannel()) } else -> {} } part.dispose() } call.respondText("$fileDescription is uploaded to 'uploads/$fileName'") } } }

    Send JSON data

    To serialize and deserialize JSON data in POST/PUT requests, install the ContentNegotiation plugin to a new client.

    Inside the request, you can specify the Content-Type header using the contentType() function and the request body using the setBody() function.

    import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.testing.* import kotlin.test.* class CustomerTests { @Test fun testPostCustomer() = testApplication { application { main() } client = createClient { install(ContentNegotiation) { json() } } val response = client.post("/customer") { contentType(ContentType.Application.Json) setBody(Customer(3, "Jet", "Brains")) } assertEquals("Customer stored correctly", response.bodyAsText()) assertEquals(HttpStatusCode.Created, response.status) } }
    import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.* import kotlinx.serialization.json.* import io.ktor.server.util.getValue @Serializable data class Customer(val id: Int, val firstName: String, val lastName: String) install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true }) } post("/customer") { val customer = call.receive<Customer>() customerStorage.add(customer) call.respondText("Customer stored correctly", status = HttpStatusCode.Created) } }

    Preserve cookies during testing

    To preserve cookies between requests, install the HttpCookies plugin to a new client.

    In the following example, the reload count increases after each request due to cookies being preserved:

    package cookieclient import io.ktor.client.plugins.cookies.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testRequests() = testApplication { application { main() } val client = createClient { install(HttpCookies) } val loginResponse = client.get("/login") val response1 = client.get("/user") assertEquals("Session ID is 123abc. Reload count is 1.", response1.bodyAsText()) val response2 = client.get("/user") assertEquals("Session ID is 123abc. Reload count is 2.", response2.bodyAsText()) val response3 = client.get("/user") assertEquals("Session ID is 123abc. Reload count is 3.", response3.bodyAsText()) val logoutResponse = client.get("/logout") assertEquals("Session doesn't exist or is expired.", logoutResponse.bodyAsText()) } }
    import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.sessions.* import io.ktor.util.* import kotlinx.serialization.Serializable @Serializable data class UserSession(val id: String, val count: Int) fun Application.main() { install(Sessions) { val secretEncryptKey = hex("00112233445566778899aabbccddeeff") val secretSignKey = hex("6819b57a326945c1968f45236589") cookie<UserSession>("user_session") { cookie.path = "/" cookie.maxAgeInSeconds = 10 transform(SessionTransportTransformerEncrypt(secretEncryptKey, secretSignKey)) } } routing { get("/login") { call.sessions.set(UserSession(id = "123abc", count = 0)) call.respondRedirect("/user") } get("/user") { 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.") } }

    Test HTTPS

    To test an HTTPS endpoint, set the request protocol using the URLBuilder.protocol property:

    @Test fun testRoot() = testApplication { application { module() } val response = client.get("/") { url { protocol = URLProtocol.HTTPS } } assertEquals("Hello, world!", response.bodyAsText()) }

    Test WebSockets

    You can test WebSocket conversations by using the WebSockets client plugin:

    package com.example import io.ktor.client.plugins.websocket.* import io.ktor.websocket.* import io.ktor.server.testing.* import kotlin.test.* class ModuleTest { @Test fun testConversation() { testApplication { application { module() } val client = createClient { install(WebSockets) } client.webSocket("/echo") { val greetingText = (incoming.receive() as? Frame.Text)?.readText() ?: "" assertEquals("Please enter your name", greetingText) send(Frame.Text("JetBrains")) val responseText = (incoming.receive() as Frame.Text).readText() assertEquals("Hi, JetBrains!", responseText) } } } }

    End-to-end testing with HttpClient

    You can use the Ktor HTTP client for a full end-to-end testing of your server application.

    In the example below, the HTTP client makes a test request to the TestServer:

    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("http://localhost:8080/").body() assertEquals("Hello, world!", response) } }
    20 January 2026