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-hostdependency 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-testdependency 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:
Define a test using
testApplication {}.Configure and run a test instance of your application.
Optionally, configure the HTTP client.
Use the client to make HTTP requests to your test application and receive responses.
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.
Locate or create the
src/test/kotlindirectory in your project.Create a new Kotlin file (for example,
ApplicationTest.kt).Define a Kotlin class that will contain your tests:
class ApplicationTest { // Test functions go here }
Add a test function annotated with
@Test. Inside the test, use thetestApplication {}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 = "00112233445566778899aabbccddeeff".hexToByteArray() val secretSignKey = "6819b57a326945c1968f45236589".hexToByteArray() 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) } }
24 April 2026