How to inject dependencies in a ktor Application

Viewed 11526

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.

4 Answers

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!

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")

Feel free to change that with 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.

Kodein DI has good Ktor support, see the corresponding documentation for more details.

Example code from the documentation:

fun main(args: Array<String>) {
    embeddedServer(Netty, port = 8080) {
        di { 
            bind<Random>() with singleton { SecureRandom() } 
        }

        routing {
            get("/") {
                val random by di().instance<Random>() 
                /* logic here */
            }
        }
   }.start(true)
}
Related