How to inject dependencies in a ktor Application

17.5k views Asked by At

The documentation talks about dependency injection but does not really show how it is being done.

Documentation is not completed as well and has a bunch of place holders: http://ktor.io/getting-started.html

I tried to create my main function in a way that it accepts parameter (which is my dependency) but that failed on the test side when I call withTestApplication. I looked into the application code and saw that Application accepts a configuration object but I have no idea how I can change that configuration object to inject some dependencies inside of it.

package org.jetbrains.ktor.application

/**
 * Represents configured and running web application, capable of handling requests
 */
class Application(val environment: ApplicationEnvironment) : ApplicationCallPipeline() {
    /**
     * Called by host when [Application] is terminated
     */
    fun dispose() {
        uninstallAllFeatures()
    }
}

/**
 * Convenience property to access log from application
 */
val Application.log get() = environment.log

In the test code using withTestApplication I have something similar to the below:

@Test
internal fun myTest() = withTestApplication (Application::myMain)

The above withTestApplication would fail if I call myMain with parameters (parameters that I need to mock and inject.)

Update:

The issue is that in my request handling, I am using a dependency class that connects to other web services outside and does some requests, I need a way to be able to inject this so in my tests I can stub/mock it and change its behavior based on my test cases.

3

There are 3 answers

1
Ilya Ryzhenkov On

Ktor doesn't have a built-in dependency injection mechanism. If you need to use DI, you will need to use any framework you like, such as Guice for example. It would look something like this:

fun Application.module() {
  Guice.createInjector(MainModule(this))
}

// Main module, binds application and routes
class MainModule(private val application: Application) : AbstractModule() {
    override fun configure() {
        bind(Application::class.java).toInstance(application)
        ... other bindings ...
    }
}

This way you delegate application composition to Guice and build it up as any other application. E.g. you might compose different parts of your application like this:

class Hello @Inject constructor(application: Application) {
  init {
    application.routing {
        get("/") {
            call.respondText("Hello")
        }
    }
  }
}

and then bind it in a main module:

bind(Hello::class.java).asEagerSingleton()

asEagerSingleton is needed so that Guice will create it eagerly since no other service would query it.

0
Alkis Mavridis On

After experimenting a bit with Koin, Kodein, and Daggers, we ended up using spring-context with Ktor. It works like a charm.

Step 1: In our Gradle file:

implementation(group = "org.springframework", name = "spring-context", version = "5.3.5")

Change that to any spring-context version you prefer.

Step 2: We defined our @Components, @Configurations, @Beans etc. like we would with any spring application.

Step 3: in our main method, we have a little bit of glue code to explicitly initialize the DI context and use it for initializing Ktor:

    val ctx = AnnotationConfigApplicationContext("YOUR.ROOT.PACKAGE.GOES.HERE")
    val someBean = ctx.getBean(SomeBean::class.java) // you can get any bean you need to use in your top-level glue code

    // ... go ahead with KTOR configuration as usual. You can access any bean using the ctx variable.

Of course this glue code where we explicitly interact with the spring context is done only once. The rest of the project consist of components that reference each other using the regular spring-context way.

0
Pavel Shorokhov On

Easy example with Koin

1) At first, define our prod and test dependencies:

val prodModule = module {
    single<IFirstService> { RealFirstService() }
    single<ISecondService> { RealSecondService() }
}

val testModule = module {
    single<IFirstService> { FakeFirstService() }
    single<ISecondService> { FakeSecondService() }
}

2) Then add DI initialization before app start:

fun main(args: Array<String>) {
    startKoin(listOf(prodModule))
    embeddedServer(Netty, commandLineEnvironment(args)).start(true)
}

3) Use inject in Application or in routes:

fun Application.apiModule() {
    val firstService: IFirstService by inject()
    val secondService: ISecondService by inject()
    ...
    routing {
        someApi(inject(), inject())
    }
}

4) (Optional) For tests just add initialization in testModule before run test:

fun testApp(test: TestApplicationEngine.() -> Unit) {
    withTestApplication({
        ... // configure your test app here

        stopKoin() // Need to stop koin and restart after other tests
        startKoin(listOf(testModule)) // Init with test DI

        apiModule() // Run you application
    })
}

// And run tests
@Test
fun `get events`() = testApp {
    // do tests
}

That's all!