Search code examples
kotlinktorkodein

Ktor / Kodein - How to write Integration Tests


Currently I write a small demo-app which uses Ktor as its Application Environment and Kodein as the Dependency Injection Framework.

During the initialization of the Application I do import some modules, one of those I would like to replace during the initialization of the Integration Tests:

fun Application.module(testing: Boolean = false) {
logger.debug { "Starting main" }

restModule()

di {
  bind<Json>() with singleton {
    Json {
      ...
    }
  }

  import(persistenceModule)
}

In the test, I would like to use a different persistenceModule, say eg. a MemoryModule. My tests are initialized like:

fun start() {
  val configPath = ClassLoader.getSystemResource("application-acceptanceTest.conf").file
  engine = embeddedServer(CIO, commandLineEnvironment(arrayOf("-config=$configPath")))
  engine.start()

  val disposable = engine.environment.monitor.subscribe(ApplicationStarted) { application: Application ->
    started = true
  }

  while (!started) {
    Thread.sleep(10)
  }
  disposable.dispose()
}

I have tried already to call

engine.application.di

but this gives me (quite obviously) only access to the Ktor Feature, which is already initialized. Is anything like this possible at all?


Solution

  • Kodein-DI allows you to override dependencies. Regarding the following interface:

    interface Repository {
        fun save()
        fun find()
    }
    

    You can have a production implementation, included in its own DI module:

    class PersistenceRepository : Repository {
        /* implementation */
    }
    
    val persistenceModule = DI.Module("persistenceModule") {
        bind<Repository>() with singleton { PersistenceRepository() }
    }
    

    and also a test implementation of that same interface:

    class MemoryRepository : Repository {
        /* implementation */
    }
    
    val memoryModule = DI.Module("memoryModule") {
        bind<Repository>(overrides = true) with singleton { MemoryRepository() }
    }
    

    Note the overrides parameter that needs to be explicit.

    You can pass a DI container to your Ktor function:

    val mainDI = DI {
        import(persistenceModule)
    }
    
    fun Application.main(di: DI) {
        di { extend(di) }
    }
    

    And extend the mainDI in your tests, to override the proper bindings with the memoryModule:

    class ApplicationTest {
        val testDI = DI {
            extend(mainDI)
            import(memoryModule, allowOverride = true)
        }
        
        @Test
        fun myTest() {
            withTestApplication({ main(testDI) })
            // ...
        }
    }