https://kotlinlang.org logo
#ktor
Title
# ktor
a

Adam Papenhausen

11/07/2023, 12:15 AM
Hello all! I'm looking for some guidance on approaches to unit testing Ktor client plugins. We use Hilt for the DI layer and I'm having trouble reasoning about the best way to wrap the Ktor plugin for testing. For instance, take the following OkHttp interceptor:
Copy code
/** Adds the package name as headers for every request. */
class BuildMetadataMiddleware @Inject constructor(private val packageInfoProvider: PackageInfoProvider) : Interceptor {
  companion object {
    const val ClientNameHeader = "X-Client-Name"
  }

  override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(
    chain.request().newBuilder()
      .addHeader(ClientNameHeader, packageInfoProvider.packageName)
      .build()
  )
}
I can then create a test client and assert on the
intercept
method with a fake
PackageInfoProvider
. With Ktor, it doesn't immediately seem that you want to subclass the
ClientPlugin
interface but rather provide a plugin config class, which to my understanding requires a default value. I have two questions: • How can I implement this if I do not want a default value (always have it be provided by the injected dependency) • How can a plugin fail safely? Would I need to create custom exceptions and handle them or is there an API like
request.cancel(reason)
? This is what the above impl could look like in Ktor:
Copy code
class BuildMetadataConfig {
  val packageName: String? = null
}

val buildMetadataPlugin = createClientPlugin("BuildMetadata", ::BuildMetadataConfig) {
  val packageName = pluginConfig.packageName

  onRequest { request, _ ->
    request.header("X-Client-Name", packageName)
  }
}
a

Aleksei Tirman [JB]

11/07/2023, 8:20 AM
So do you want to test that the client with the installed plugin sends the
X-Client-Name
header on each request?
a

Adam Papenhausen

11/07/2023, 3:21 PM
Yes, but also that it can pull the value of the header dynamically from a dependency on every request.
a

Aleksei Tirman [JB]

11/07/2023, 3:23 PM
Do you mean that the header value is configurable?
a

Adam Papenhausen

11/07/2023, 3:24 PM
In this case just the header value, which is provided by a
PackageInfoProvider
interface that gets injected where the client is created
For example, I think one way to make the abstraction is like this:
Copy code
val buildMetadataPlugin = fun(packageInfoProvider: PackageInfoProvider) = createClientPlugin(...)
a

Aleksei Tirman [JB]

11/07/2023, 3:30 PM
You can test the plugin using the MockEngine (https://ktor.io/docs/http-client-testing.html).
a

Adam Papenhausen

11/07/2023, 3:33 PM
Okay, for the other part of my question, if a client's plugin fails, is it recommended to throw an exception and catch it where the request was made or is there a different recommended approach to cancelling/failing a request?
a

Aleksei Tirman [JB]

11/07/2023, 3:34 PM
What kind of failure are you talking about?
a

Adam Papenhausen

11/07/2023, 3:37 PM
Let's say for instance I have a plugin that tries to encode some data in the request body as JSON but the encoding itself fails. Or, it tries to read a header value (like in the example above) from a cache/db but it is not there. I want to short-circuit the request and be able to catch the failure. In OkHttp the way to do that is by throwing an exception - wondering if Ktor has a different approach?
Or is it better that plugins do not handle these types of failure modes?
a

Aleksei Tirman [JB]

11/08/2023, 6:45 AM
In case of failure, you can throw an exception, and so the execution context of the request will be canceled with the reason.