Testing Server Applications

Ktor is designed to allow the creation of applications that are easily testable. And of course,Ktor infrastructure itself is well tested with unit, integration, and stress tests.In this section, you will learn how to test your applications.

Table of contents:

TestEngine

Ktor has a special kind engine TestEngine, that doesn’t create a web server, doesn’t bind to sockets and doesn’t doany real HTTP requests. Instead, it hooks directly into internal mechanisms and processes ApplicationCall directly. This allows for fast test execution at the expense of maybe missing some HTTP processing details. It’s perfectly capable of testing application logic, but be sure to set up integration tests as well.

A quick walkthrough:

  • Add ktor-server-test-host dependency to the test scope
  • Create a JUnit test class and a test function
  • Use withTestApplication function to setup a test environment for your Application
  • Use the handleRequest function to send requests to your application and verify the results

Building post/put bodies

application/x-www-form-urlencoded

When building the request, you have to add a Content-Type header:

  1. addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())

And then set the bodyChannel, for example, by calling the setBody method:

  1. setBody("name1=value1&name2=value%202")

Ktor provides an extension method to build a form urlencoded out of a List of key/value pairs:

  1. fun List<Pair<String, String>>.formUrlEncode(): String

So a complete example to build a post request urlencoded could be:

  1. val call = handleRequest(HttpMethod.Post, "/route") {
  2. addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
  3. setBody(listOf("name1" to "value1", "name2" to "value2").formUrlEncode())
  4. }

multipart/form-data

When uploading big files, it is common to use the multipart encoding, which allows sendingcomplete files without preprocessing. Ktor’s test host provides a setBody extension methodto build this kind of payload. For example:

  1. val call = handleRequest(HttpMethod.Post, "/upload") {
  2. val boundary = "***bbb***"
  3. addHeader(HttpHeaders.ContentType, ContentType.MultiPart.FormData.withParameter("boundary", boundary).toString())
  4. setBody(boundary, listOf(
  5. PartData.FormItem("title123", { }, headersOf(
  6. HttpHeaders.ContentDisposition,
  7. ContentDisposition.Inline
  8. .withParameter(ContentDisposition.Parameters.Name, "title")
  9. .toString()
  10. )),
  11. PartData.FileItem({ byteArrayOf(1, 2, 3).inputStream().asInput() }, {}, headersOf(
  12. HttpHeaders.ContentDisposition,
  13. ContentDisposition.File
  14. .withParameter(ContentDisposition.Parameters.Name, "file")
  15. .withParameter(ContentDisposition.Parameters.FileName, "file.txt")
  16. .toString()
  17. ))
  18. ))
  19. }

Defining configuration properties in tests

In tests, instead of using an application.conf to define configuration properties,you can use the MapApplicationConfig.put method:

  1. withTestApplication({
  2. (environment.config as MapApplicationConfig).apply {
  3. // Set here the properties
  4. put("youkube.session.cookie.key", "03e156f6058a13813816065")
  5. put("youkube.upload.dir", tempPath.absolutePath)
  6. }
  7. main() // Call here your application's module
  8. })

HttpsRedirect feature

The HttpsRedirect changes how testing is performed.Check the testing section of the HttpsRedirect feature for more information.

Testing several requests preserving sessions/cookies

You can easily test several requests in a row keeping the Cookie information among them. By using the cookiesSession method.This method defines a session context that will hold cookies, and exposes a CookieTrackerTestApplicationEngine.handleRequestextension method to perform requests in that context.

For example:

  1. @Test
  2. fun testLoginSuccessWithTracker() = testApp {
  3. val password = "mylongpassword"
  4. val passwordHash = hash(password)
  5. every { dao.user("test1", passwordHash) } returns User("test1", "test1@test.com", "test1", passwordHash)
  6. cookiesSession {
  7. handleRequest(HttpMethod.Post, "/login") {
  8. addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
  9. setBody(listOf("userId" to "test1", "password" to password).formUrlEncode())
  10. }.apply {
  11. assertEquals(302, response.status()?.value)
  12. assertEquals("http://localhost/user/test1", response.headers["Location"])
  13. assertEquals(null, response.content)
  14. }
  15. handleRequest(HttpMethod.Get, "/").apply {
  16. assertTrue { response.content!!.contains("sign out") }
  17. }
  18. }
  19. }

Note: cookiesSession is not included in Ktor itself, but you can add this boilerplate to use it:

  1. fun TestApplicationEngine.cookiesSession(
  2. initialCookies: List<Cookie> = listOf(),
  3. callback: CookieTrackerTestApplicationEngine.() -> Unit
  4. ) {
  5. callback(CookieTrackerTestApplicationEngine(this, initialCookies))
  6. }
  7. class CookieTrackerTestApplicationEngine(
  8. val engine: TestApplicationEngine,
  9. var trackedCookies: List<Cookie> = listOf()
  10. )
  11. fun CookieTrackerTestApplicationEngine.handleRequest(
  12. method: HttpMethod,
  13. uri: String,
  14. setup: TestApplicationRequest.() -> Unit = {}
  15. ): TestApplicationCall {
  16. return engine.handleRequest(method, uri) {
  17. val cookieValue = trackedCookies.map { (it.name).encodeURLParameter() + "=" + (it.value).encodeURLParameter() }.joinToString("; ")
  18. addHeader("Cookie", cookieValue)
  19. setup()
  20. }.apply {
  21. trackedCookies = response.headers.values("Set-Cookie").map { parseServerSetCookieHeader(it) }
  22. }
  23. }

Example with dependencies

See full example of application testing in ktor-samples-testable.Also, most ktor-samples modules provideexamples of how to test specific functionalities.

In some cases we will need some services and dependencies. Instead of storing them globally, we suggest youto create a separate function receiving the service dependencies. This allows you to pass different(potentially mocked) dependencies in your tests:

test.kt

  1. class ApplicationTest {
  2. class ConstantRandom(val value: Int) : Random() {
  3. override fun next(bits: Int): Int = value
  4. }
  5. @Test fun testRequest() = withTestApplication({
  6. testableModuleWithDependencies(
  7. random = ConstantRandom(7)
  8. )
  9. }) {
  10. with(handleRequest(HttpMethod.Get, "/")) {
  11. assertEquals(HttpStatusCode.OK, response.status())
  12. assertEquals("Random: 7", response.content)
  13. }
  14. with(handleRequest(HttpMethod.Get, "/index.html")) {
  15. assertFalse(requestHandled)
  16. }
  17. }
  18. }

module.kt

  1. fun Application.testableModule() {
  2. testableModuleWithDependencies(
  3. random = SecureRandom()
  4. )
  5. }
  6. fun Application.testableModuleWithDependencies(random: Random) {
  7. routing {
  8. get("/") {
  9. call.respondText("Random: ${random.nextInt(100)}")
  10. }
  11. }
  12. }

build.gradle

  1. // ...
  2. dependencies {
  3. // ...
  4. testCompile("io.ktor:ktor-server-test-host:$ktor_version")
  5. }