kotlinkotlin-coroutinesktorkotlintest

How do I get ktor testApplication client to not wait for a coroutine job in the call to complete?


In the ktor application I have a route that starts a session for the created resource that launches a job to manage interactions with that session and auto-terminates the session if there are no interactions for a while

// routing
post("/sessions") {
  val sessionName = call.receive<NewSessionRequest>().name
  val session = Sessions.newSession(sessionName) // Companion object that creates a session
  launch {
    var sessionExists = true
    while (sessionExists) {
      sessionExists = withTimeoutOrNull(session.timeToLive) {
        session.lifeChannel.receive()
      } ?: false
    }
    session.close()
    Sessions.remove(session)
  }
  call.respond(HttpStatusCode.Created, sessionName)
}

post("/sessions/{name}") {
  // other call that pings the sessions lifeChannel
}

and then some other route that triggers a message to be sent to the session's lifeChannel to keep the session alive.

In normal operation this works quite well and the session is kept alive as desired. However, during testing the test waits until the entire launched job completes before continuing.

@Test
fun `user can create room`() = testApplication {
  val response = jsonClient.post("/sessions") {
    contentTupe(ContentType.Application.Json)
    setBody(NewSessionRequest("sess"))
  }
  assertEquals(HttpStatusCodes.created, response.status)
}

Will wait until the job completes and the session terminates before completing the test.

How can I get the testApplication to ignore or work outside of the coroutine context of the application as a normal http call would do with a real ktor app? Is this a bad practice as testing for it is non-intuitive?

Edit

Adding a simple test I want to have pass

class CoroutineRoutesTest {
    @Test
    fun `how to deal with launched coroutine`() {
        testApplication {
            application {
                routing {
                    get("/launch-job") {
                        val delay = 1.seconds
                        launch {
                            delay(delay)
                        }
                        call.respond("Job lasting for $delay")
                    }
                }
            }
            val response: HttpResponse
            val timing = measureTimeMillis {
                response = client.get("/launch-job")
            }
            assertEquals(HttpStatusCode.OK, response.status)
            LoggerFactory.getLogger("CoroutineTest").info("response time: $timing ms")
            assertTrue(timing < 800)
        }
    }
}

Solution

  • I can make your last test pass by launching a job from a different coroutine scope. I'm not sure whether this is the desired behavior or not.

    @Test
    fun `how to deal with launched coroutine`() {
        val scope = CoroutineScope(Dispatchers.Default)
        testApplication {
            application {
                routing {
                    get("/launch-job") {
                        val delay = 1.seconds
                        scope.launch {
                            delay(delay)
                        }
                        call.respond("Job lasting for $delay")
                    }
                }
            }
            // ...
        }
    }