ktorkotestktor-server

simplest possible ktor-server-test-host kotest fails with connection refused


I've written the simplest test I can with ktor-server-test-host and I get a Connection refused.

My imports:

ktor:3.1.2 (including ktor-server-test-host)
kotest:5.9.1
kotlin:2.1.20 (JVM)

Here's my test code:

package bps.budget.server

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.http.withCharset
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import io.ktor.server.testing.testApplication

class SimpleKtorTest : FreeSpec() {

    init {
        testApplication {
            application {
                routing {
                    get("/") {
                        call.respondText("root")
                    }
                }
            }
            "/" {
                val response: HttpResponse = client.get("/")
                response.status shouldBe HttpStatusCode.OK
                response.contentType() shouldBe ContentType.Text.Plain.withCharset(Charsets.UTF_8)
                response.bodyAsText() shouldBe "root"
            }
        }
    }
}

I get this error when I run the test:

Parent job is Completed
kotlinx.coroutines.JobCancellationException: Parent job is Completed; job=SupervisorJobImpl{Completed}@301c7f3e
    at bps.budget.server.SimpleKtorTest$1$2.invokeSuspend(SimpleKtorTest.kt:51)
    at bps.budget.server.SimpleKtorTest$1$2.invoke(SimpleKtorTest.kt)
    at bps.budget.server.SimpleKtorTest$1$2.invoke(SimpleKtorTest.kt)
    at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:42)
    at bps.budget.server.SimpleKtorTest$1$2.invokeSuspend(SimpleKtorTest.kt:51)
Caused by: kotlinx.coroutines.JobCancellationException: Parent job is Completed; job=SupervisorJobImpl{Completed}@301c7f3e

When I create my own client using simply val client = createClient {}, I get this stack trace:

EmbeddedServer was stopped
java.lang.IllegalStateException: EmbeddedServer was stopped
    at io.ktor.server.engine.EmbeddedServer.currentApplication(EmbeddedServerJvm.kt:111)
    at io.ktor.server.engine.EmbeddedServer.access$currentApplication(EmbeddedServerJvm.kt:36)
    at  at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:42)
    at bps.budget.server.SimpleKtorTest$1$2.invokeSuspend(SimpleKtorTest.kt:46)
    at io.kotest.core.spec.style.scopes.FreeSpecRootScope$invoke$1.invokeSuspend(FreeSpecRootScope.kt:26)
Caused by: java.lang.IllegalStateException: EmbeddedServer was stopped
    at io.ktor.server.engine.EmbeddedServer.currentApplication(EmbeddedServerJvm.kt:111)

Surely, I'm missing something.


Solution

  • This appears to be an issue with Kotest's FreeSpec. If you switch to something like AnnotationSpec, this works fine.

    The following code works fine:

    class SimpleKtorTest : AnnotationSpec() {
    
        @Test
        fun test() {
            testApplication {
                application {
                    routing {
                        get("/") {
                            call.respondText("root")
                        }
                    }
                }
                val client = createClient {
                }
                val response: HttpResponse = client.get("/")
                response.status shouldBe HttpStatusCode.OK
                response.contentType() shouldBe ContentType.Text.Plain.withCharset(Charsets.UTF_8)
                response.bodyAsText() shouldBe "root"
            }
        }
    }
    

    You can get it to work with FreeSpec but only in a very limited way:

    class SimpleKtorTest : FreeSpec() {
    
        init {
            "test endpoints" {
                testApplication {
                    application {
                        routing {
                            get("/") {
                                call.respondText("root")
                            }
                        }
                    }
                    val client = createClient {
                    }
                    val response: HttpResponse = client.get("/")
                    response.status shouldBe HttpStatusCode.OK
                    response.contentType() shouldBe ContentType.Text.Plain.withCharset(Charsets.UTF_8)
                    response.bodyAsText() shouldBe "root"
                }
            }
        }
    }
    

    However, if you try to nest multiple tests within the testApplication and switch the outer test container to use the -:

            "test endpoints" - {
    

    and then put multiple tests inside, the test never exits.