Skip to content

Ktor

Stove starts your real Ktor server. It can resolve beans through Koin, Ktor-DI, or a custom resolver when you register bridge().

Open Ktor + Postgres in wizard

Two knobs 1) Your run(args, wait = false, ...) returns the started Application. 2) ktor(runner = ...) calls it after systems are ready. If you use bridge(), Stove auto-detects Koin vs Ktor-DI; custom containers plug in via a resolver lambda.

Anatomy

ktor( 1 runner = { params -> com.app.run( 2 params, shouldWait = false, 3 testModules = listOf(testModule) ) }, withParameters = listOf("port=8080") )
1ktor { } registers Ktor as the AUT runner.
2runner calls your extracted run. Pass test modules / test deps here.
3shouldWait = false is critical. Stove keeps the suite alive itself.

Setup

dependencies {
    testImplementation(platform("com.trendyol:stove-bom:$stoveVersion"))
    testImplementation("com.trendyol:stove")
    testImplementation("com.trendyol:stove-ktor")
    testImplementation("com.trendyol:stove-extensions-kotest")  // or -junit
}

Extract run to accept test overrides:

fun main(args: Array<String>) = run(args, shouldWait = true).let { Unit }

fun run(
  args: Array<String>,
  shouldWait: Boolean = false,
  testModules: List<Module> = emptyList()
): Application {
  val cfg = loadConfiguration<AppConfiguration>(args)
  return embeddedServer(Netty, port = cfg.port, host = "localhost") {
    install(Koin) { modules(appModule, *testModules.toTypedArray()) }
    configureRouting()
  }.start(wait = shouldWait).application
}
fun main(args: Array<String>) = run(args, shouldWait = true).let { Unit }

fun run(
  args: Array<String>,
  shouldWait: Boolean = false,
  testDependencies: (DependencyRegistrar.() -> Unit)? = null
): Application {
  val cfg = loadConfiguration<AppConfiguration>(args)
  return embeddedServer(Netty, port = cfg.port, host = "localhost") {
    install(DI) {
      dependencies {
        provide<MyService> { MyServiceImpl() }
        testDependencies?.invoke(this)
      }
    }
    configureRouting()
  }.start(wait = shouldWait).application
}

Minimal Stove().with { }:

Stove().with {
    httpClient { HttpClientSystemOptions(baseUrl = "http://localhost:8080") }
    ktor(
        runner = { params -> run(params, shouldWait = false) },
        withParameters = listOf("port=8080")
    )
}.run()

Bridge. Automatic DI detection

DI framework Detection Priority
Ktor-DI dependencies { } block active Preferred when both present
Koin install(Koin) { } active Used when Ktor-DI absent
Custom (Kodein, Dagger, etc.) Manual resolver lambda Explicit override

Test overrides

Stove().with {
  bridge()
  ktor(runner = { params ->
    run(params, shouldWait = false, testModules = listOf(
      module { single<TimeProvider>(override = true) { FixedTimeProvider() } }
    ))
  })
}.run()
Stove().with {
  bridge()
  ktor(runner = { params ->
    run(params, shouldWait = false) {
      provide<TimeProvider> { FixedTimeProvider() }
    }
  })
}.run()
Stove().with {
  bridge { application, type -> myDiContainer.resolve(type) }
  ktor(runner = { params -> run(params, shouldWait = false) })
}.run()

Using Bridge in tests

stove {
  using<UserService> {
    findById(123).name shouldBe "John"
  }

  using<List<PaymentService>> {
    forEach { it.validate() }
  }
}

Full patterns (multi-bean access, value capture, generics): Bridge reference.

What you get

  • ✅ Real Netty server, real routing
  • ✅ bridge() for Koin and Ktor-DI when the corresponding plugin is installed normally
  • ✅ Composes with every Stove system
  • ✅ Hot-swap DI containers via custom resolver

Common pitfalls

shouldWait = true hangs the suite

Production main waits; tests must not. Always pass shouldWait = false from the runner.

DI not detected

Bridge looks for install(Koin) or install(DI). If you wrap them in feature plugins, expose a custom resolver.

Example