Ktor 3.0.0-rc-1 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.

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-jvm</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 the testApplication function to set up a configured instance of a test application running locally.

  3. Use the Ktor HTTP client instance inside a test application to make a request to your server, receive a response, and make assertions.

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.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()) } }
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!") } } }

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

Test an application

Step 1: Configure a test application

A configuration of test applications might include the following steps:

Add application modules

To test an application, its modules should be loaded to testApplication. To do that, you must either explicitly load your modules or configure the environment to load them from a configuration file.

Explicit loading of modules

To add modules to a test application manually, use the application function:

fun testModule1() = testApplication { application { module1() module2() } }

Load modules from a configuration file

If you want to load modules from a configuration file, use the environment function 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.

Add routes

You can add routes to your test application using the routing function. This might be convenient for the following use-cases:

  • Instead of adding modules to a test application, you can add specific routes that should be tested.

  • You can add routes required only in a test application. The example below shows how to add 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")) } } }

    You can find the full example with a test here: auth-oauth-google.

Customize environment

To build a custom environment for a test application, use the environment function. For example, to use a custom configuration for tests, you can create a configuration file in the test/resources folder and load it using the config property:

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

Another way to specify configuration properties is using MapApplicationConfig. This might be useful if you want to access application configuration before the application starts. The example below shows how to pass MapApplicationConfig to the testApplication function using the config property:

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

Mock external services

Ktor allows you to mock external services using the externalServices function. Inside this function, you need to call the hosts function that accepts two parameters:

  • The hosts parameter accepts URLs of external services.

  • The block parameter allows you to configure the Application that acts as a mock for an external service. You can configure routing and install plugins for this Application.

The sample below shows how to use externalServices to simulate a JSON response returned by Google API:

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", "", "", "", "")) } } } } }

You can find the full example with a test here: auth-oauth-google.

Step 2: (Optional) Configure a client

The testApplication provides access to an HTTP client with default configuration using the client property. If you need to customize the client and install additional plugins, you can use the createClient function. For example, to send JSON data in a test POST/PUT request, you can install the ContentNegotiation plugin:

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

Step 3: Make a request

To test your application, use the configured client to make a request and receive a response. The example below shows how to test the /customer endpoint that handles POST requests:

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

Step 4: Assert results

After receiving a response, you can verify the results by making assertions provided by the kotlin.test library:

@Test fun testPostCustomer() = testApplication { application { main() } val 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 POST/PUT request, you need to set the Content-Type header and specify the request body. To do this, you can use the header 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.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 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

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.client.request.* import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.application.* import io.ktor.server.testing.* import io.ktor.utils.io.* import org.junit.* import java.io.* import kotlin.test.* import kotlin.test.Ignore 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)) } }
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.utils.io.* import kotlinx.io.readByteArray 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 val fileBytes = part.provider().readRemaining().readByteArray() File("uploads/$fileName").writeBytes(fileBytes) } else -> {} } part.dispose() } call.respondText("$fileDescription is uploaded to 'uploads/$fileName'") } } }

Send JSON data

To send JSON data in a test POST/PUT request, you need to create a new client and install the ContentNegotiation plugin that allows serializing/deserializing the content in a specific format. Inside a request, you can specify the Content-Type header using the contentType function and the request body using setBody. The example below shows how to test the /customer endpoint that handles POST requests.

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.application.* import io.ktor.server.testing.* import kotlinx.serialization.* import kotlinx.serialization.json.* import kotlin.test.* class CustomerTests { @Test fun testPostCustomer() = testApplication { application { main() } val 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.* @Serializable data class Customer(val id: Int, val firstName: String, val lastName: String) fun Application.main() { install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true }) } routing { post("/customer") { val customer = call.receive<Customer>() customerStorage.add(customer) call.respondText("Customer stored correctly", status = HttpStatusCode.Created) } } }

Preserve cookies during testing

If you need to preserve cookies between requests when testing, you need to create a new client and install the HttpCookies plugin. In a test below from the session-cookie-client example, reload count is increased after each request since cookies are preserved.

import io.ktor.client.plugins.cookies.* 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 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

If you need to test an HTTPS endpoint, change the protocol used to make a request using the URLBuilder.protocol property:

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 testRoot() = testApplication { application { module() } val response = client.get("/") { url { protocol = URLProtocol.HTTPS } } assertEquals("Hello, world!", response.bodyAsText()) } }

You can find the full example here: ssl-engine-main.

Test WebSockets

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

import io.ktor.client.plugins.websocket.* import io.ktor.server.application.* 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

Apart from a testing engine, you can use the Ktor HTTP client for 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) } }

For complete examples, refer to these samples:

  • embedded-server: a sample server to be tested.

  • e2e: contains helper classes and functions for setting up a test server.

Last modified: 11 September 2024