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:
Testing overview
To use a testing engine, follow the steps below:
Create a JUnit test class and a test function.
Use withTestApplication function to set up a test environment for your application.
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.
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.
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")
}
}
}
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:
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: 11 May 2022