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:
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/kotlin directory 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 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.
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:
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:
The testApplication {} function provides a configured HTTP client through the client property. To customize the client and install additional plugins, use the createClient {} function.
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.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.")
}
}