Hey everyone! :android-wave: So I've created a fai...
# ktor
r
Hey everyone! 👋 So I've created a fairly simple network library using Ktor following the official guidelines, sharing common code in a multiplatform project. I've configured the engines following the example in the official guidelines but I also wanted to follow the official guidelines for unit testing the Ktor clients but I struggle really hard with making this work and making both solutions work simultaneously. Can someone help me either figuring out how to solve this issue or providing a solution right away, please?
a
What problems do you experience?
r
Yeah, the problem... The testing guideline says that the engine should be a constructor parameter for the client in order to be exchangeable with a
MockEngine
in tests, but the example for configuring engines in multiplatform projects says that the engine should be defined only by the platform and not by the client implementation, but this way I cannot exchange the engine in tests.
Basically the question is; How can I configure Ktor clients in multiplatform project with the ability to exchange the engine with
MockEngine
in tests?
a
I think you have to make the engine definable by the client implementation with a default engine defined by the platform.
r
Hmm 🤔 So the clients may have the engine param in the constructor and that'd be a nullable type with a default value of null, so that the platform implementations can supply their respective engine in case the client didn't supply an other one (a MovkEngine) explicitly?
a
Yeah, that could be a solution.
r
Yeah, that can work. Thanks for your help, I'll check this out! 👍🏻
Now I can run the tests, but face another issue which I'm not even sure about what it is.
No transformation found: class io.ktor.utils.io.ByteBufferChannel (Kotlin reflection is not available) -> class dev.drathar.poktor.model.move.MoveTarget (Kotlin reflection is not available)
with response from http://localhost/api/v2/move-target/specific-move:
status: 200 OK
response headers:
Content-Type: application/json
I don't have the slightest clue on why the requests are targeting
localhost
. The client configuration I'm using is
Copy code
internal class DefaultApiClient(mockEngine: HttpClientEngine? = null) : ApiClient {
    override val httpClient = httpClient(mockEngine!!) {
        defaultRequest {
            url {
                protocol = URLProtocol.HTTPS
                host = "<http://pokeapi.co|pokeapi.co>"
                path("api/v2/")
            }
        }
        install(HttpCache)
        install(ContentNegotiation) {
            json(Json {
                prettyPrint = true
                isLenient = true
                ignoreUnknownKeys = true
            })
        }
    }
}
where the
httpClient
function is declared as
Copy code
expect fun httpClient(mockEngine: HttpClientEngine? = null, config: HttpClientConfig<*>.() -> Unit = {}): HttpClient
and defined on JVM as
Copy code
actual fun httpClient(mockEngine: HttpClientEngine?, config: HttpClientConfig<*>.() -> Unit) = mockEngine?.let(::HttpClient) ?: HttpClient(OkHttp) {
    config(this)

    engine {
        config {
            retryOnConnectionFailure(true)
            connectTimeout(0, TimeUnit.SECONDS)
        }
    }
}
All my tests are currently in the
commonTest
module. Do you have any idea on what the problem might be @Aleksei Tirman [JB]?
a
What engine is used in tests? Can you share code for some test?
r
An implementation of a client would look like this
Copy code
internal class BerriesApiClient(mockEngine: HttpClientEngine? = null) : BerriesApi, ApiClient by DefaultApiClient(mockEngine) { ... }
and in a corresponding test I'd create the client as
Copy code
private fun createClient(response: String): BerriesApiClient {
    val mockEngine = MockEngine {
        respond(
            content = response, headers = headersOf(HttpHeaders.ContentType, "application/json")
        )
    }

    return BerriesApiClient(mockEngine)
}
Hey @Aleksei Tirman [JB]! 👋🏻 Any update on this? Any ideas regarding?
a
It's difficult to say something just by looking at the code above. The error tells that the expected type of a response or request body is
MoveTarget
but the body hasn't been transformed to that type (usually by the
ContentNegotiation
plugin).
r
Yes, but it also tells that the response is from
localhost
, isn't that sus? I mean, if the
MockEngine
would be used it shouldn't be the case, right?
a
Most likely it would happen with the
MockEngine
because if you don't explicitly specify the host then the
localhost
is assumed.
r
It's just interesting that if I don't configure platform-specific engines, but simply expect it as a parameter at runtime
Copy code
internal class DefaultApiClient(engine: HttpClientEngine) : ApiClient {
    override val httpClient = HttpClient(engine) {
        defaultRequest {
            url {
                protocol = URLProtocol.HTTPS
                host = "<http://pokeapi.co|pokeapi.co>"
                path("api/v2/")
            }
        }
        install(HttpCache)
        install(ContentNegotiation) {
            json(Json {
                prettyPrint = true
                isLenient = true
                ignoreUnknownKeys = true
            })
        }
    }
}
and don't modify the unit tests, everything works fine, but as soon as I replace the
HttpClient
with the platform-specific one (which AFAIK inherits the
ContentNegotiation
plugin from the shared config defined above) it just falls apart. I'll have time to dig deeper into this tomorrow, I'll try to identify the root cause.
You were right, it was the absence of the
ContentNegotiation
plugin, because I didn't config the
HttpClient
properly with the optional
MockEngine
. The code before
Copy code
actual fun httpClient(mockEngine: HttpClientEngine?, config: HttpClientConfig<*>.() -> Unit) = mockEngine?.let(::HttpClient) ?: HttpClient(OkHttp) {
    config(this)
    ...
}
and after
Copy code
actual fun httpClient(mockEngine: HttpClientEngine?, config: HttpClientConfig<*>.() -> Unit) = mockEngine?.let { HttpClient(it) { config(this) } } ?: HttpClient(OkHttp) {
    config(this)
    ...
}
See how I simply created the
HttpClient
using the default constructor without configuring the engine properly before.