Search code examples
kotlin-coroutinesktorkotlin-multiplatform-mobile

Ktor client unit test hangs / freezes within CoroutineScope.launch()


I'm having an issue in my KMM project testing a ktor client request that is launched async within a new scope. For testing purposes I pass in Dispatchers.Unconfined as the context of the new scope (In actual production code I'm using newSingleThreadContext()).

I've created an extremely simplified version of the hanging ktor request below:

@ExperimentalCoroutinesApi
@Test
fun testExample(): Unit {
    val scope = CoroutineScope(Dispatchers.Unconfined)
    scope.launch {
        val client = HttpClient { BrowserUserAgent() }

        // This line hangs
        val response : HttpResponse = client.get("https://google.com")

        // Will never get here
        println("Response: $response")
        fail("This test should fail")
    }
}

Note that if you don't call within the CoroutineScope.launch then it works fine. Then hang/ freeze only occurs when called within a CoroutineScope.launch. Again this is an extrememly simplified example, but in my actual code the reason have it setup this way is so that I can process some data in a background thread before ultimately making the ktor request - hence the CoroutineScope.launch. Also note that my code seems to work fine when running on an iOS simulator. It only hangs when running as a unit test.

Am I missing something to make this work, or is this a bug?


Solution

  • The solution for me ended up being to run my tests within runBlocking() scope, and to inject its coroutineContext into my class to be used when running CoroutineScope.launch().

    object RequestService {
      private val requestContext = newSingleThreadContext("request")
      var testContext by AtomicReference<CoroutineContext?>(null)
    
      fun makeRequest(block : () -> Unit) {
        CoroutineScope.launch(testContext ?: requestContext) {
          block()
        }
      }
    }
    
    @Test
    fun testExample() = runBlocking {
        RequestService.testContext = this.coroutineContext
        RequestService.makeRequest() {
            println("Running Ktor coroutine test")
            val client = HttpClient { BrowserUserAgent() }
    
            // This line doesn't hang anymore when running within runBlocking coroutineContext
            val response : HttpResponse = client.get("https://google.com")
            println("Response: $response")
        }
        println("Finished running Ktor coroutine test")
    }
    

    Hopefully this helps someone else attempting to create a cross-platform testable service. Thanks Aleksei for comment helping me get there.