Is there any way to mimic the configuration of an ...
# ktor
j
Is there any way to mimic the configuration of an
HttpClient
using a different engine? Let's say I have the following in my production code:
Copy code
val DEFAULT_CLIENT = HttpClient() {
    install(JsonFeature) {
        serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
            ignoreUnknownKeys = true
        })
    }
}
and then in my testing code I have the following:
Copy code
val client = HttpClient(MockEngine) {
    engine {
        addHandler { request ->
            respond(...)
        }
    }
    install(JsonFeature) {
        serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
            ignoreUnknownKeys = true
        })
    }
}
As you can see, the client configuration must be the same, so I can reproduce the client behaviour in the tests, and then I configure the
MockEngine
to respond whatever I want... I have found the following: •
HttpClient
has a constructor which takes an
engine
and a
userConfig
, but ◦
userConfig
is declared as
private val
and I can't find any public getter ◦ I don't know if I can create a shareable
HttpClientConfig
, as ▪︎ I don't explicitly use an engine in the production code, as this is a multiplatform project (iOS / Android) and I let Ktor to select the default engine per platform ▪︎
HttpClientConfig
has a type parameter
HttpClientEngineConfig
which seems to depend on the engine • You have a config method, which lets you build a new
HttpClient
with additional configuration, but you can't switch to a different engine using this So the only way I can think of to be able to do this is try to declare a shared
HttpClientConfig<*>.() -> Unit
fun
which then I can use in both instantiations. Anyway this seems like a common use case with a bit convoluted solution. Is there any other simpler way to do what I want to do?
Using the shared
fun
approach I've come up with this:
Copy code
val CLIENT_CONFIGURATION: HttpClientConfig<*>.() -> Unit = {
    install(JsonFeature) {
        serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
            ignoreUnknownKeys = true
        })
    }
}

val DEFAULT_CLIENT = HttpClient {
    apply(CLIENT_CONFIGURATION)
}

val TEST_CLIENT = HttpClient(MockEngine) {
    apply(CLIENT_CONFIGURATION)
    engine {
        addHandler { request ->
            respond(...)
        }
    }
}
It's even quite readable, as you apply a configuration to a client. Any thoughts?
r
I usually do something like
Copy code
class ApiClient(engine: HttpClientEngine) {
    private val httpClient = HttpClient(engine) {
        // shared client config
    }
}
Then in prod I pass the relevant platform engine like
ApiClient(Ios.create()
or
ApiClient(Android.create()
and in tests
ApiClient(MockEngine { ... })
2
j
ok, I see, you can use a pre-configured
MockEngine
... I didn't know you could do that, thanks!
So you don't have to explicitly create the engine for a given platform:
Copy code
private val CLIENT_CONFIGURATION: HttpClientConfig<*>.() -> Unit = {
    install(JsonFeature) {
        serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
            ignoreUnknownKeys = true
        })
    }
}

private fun client(engine: HttpClientEngine? = null): HttpClient =
    if (engine != null) HttpClient(engine) { apply(CLIENT_CONFIGURATION) }
    else HttpClient { apply(CLIENT_CONFIGURATION) }
and then invoke whenever you need a client inside the API client. And in the test code:
Copy code
val mockEngine = MockEngine { request ->
    respond(...)
}
and pass it as a parameter to the constructor / method.
r
I like passing the platform engine explicitly because it lets you do engine-specific config if needed. But if you don't need that then what you have should work fine.
j
Well, this way you have both, you can just omit the engine, and you'll have the default one, or you can pass one (either a
MockEngine
or a real one) and configure it as you want
thanks a lot for your tip, by the way 🙂
👍 1