Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ apiValidation {
"spring-4x-kafka-tests",
"spring-4x-tests",
"spring-3x-tests",
"spring-2x-tests"
"spring-2x-tests",
"ktor-di-tests",
"ktor-koin-tests",
"ktor-test-fixtures",
)
}
kover {
Expand Down
47 changes: 46 additions & 1 deletion docs/release-notes/0.20.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,54 @@ addInitializers(stoveSpring4xRegistrar { registerBean<MyService>() })

---

### Ktor DI Flexibility

Stove's Ktor module now supports multiple dependency injection systems. Previously, Koin was required. Now you can use:

1. **Koin** (existing support)
2. **Ktor-DI** (new built-in support)
3. **Custom resolver** (any DI framework)

Both Koin and Ktor-DI are now `compileOnly` dependencies - you bring your preferred DI system.

**Using Koin:**

```kotlin
dependencies {
testImplementation("io.insert-koin:koin-ktor:$koinVersion")
}

// In your test setup
bridge() // Auto-detects Koin
```

**Using Ktor-DI:**

```kotlin
dependencies {
testImplementation("io.ktor:ktor-server-di:$ktorVersion")
}

// In your test setup
bridge() // Auto-detects Ktor-DI
```

**Using a Custom Resolver:**

For any other DI framework (Kodein, Dagger, manual, etc.):

```kotlin
bridge { application, klass ->
// Your custom resolution logic
myDiContainer.resolve(klass.java)
}
```

---

### Runtime Version Checks

When Spring Boot or Spring Kafka is missing from the classpath, Stove now provides clear error messages:
When Spring Boot, Spring Kafka, or Ktor DI is missing from the classpath, Stove now provides clear error messages:

```
═══════════════════════════════════════════════════════════════════════════════
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
ktor-server-di = { module = "io.ktor:ktor-server-di", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
Expand Down
3 changes: 3 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ include(
)
include(
"starters:ktor:stove-ktor-testing-e2e",
"starters:ktor:tests:ktor-test-fixtures",
"starters:ktor:tests:ktor-koin-tests",
"starters:ktor:tests:ktor-di-tests",
"starters:spring:stove-spring-testing-e2e",
"starters:spring:stove-spring-testing-e2e-kafka",
"starters:spring:tests:spring-test-fixtures",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
public final class com/trendyol/stove/testing/e2e/DependencyResolvers {
public static final field INSTANCE Lcom/trendyol/stove/testing/e2e/DependencyResolvers;
public final fun autoDetect ()Lkotlin/jvm/functions/Function2;
public final fun getKoin ()Lkotlin/jvm/functions/Function2;
public final fun getKtorDi ()Lkotlin/jvm/functions/Function2;
}

public final class com/trendyol/stove/testing/e2e/KtorApplicationUnderTest : com/trendyol/stove/testing/e2e/system/abstractions/ApplicationUnderTest {
public fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function1;Ljava/util/List;)V
public fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand All @@ -10,12 +17,19 @@ public final class com/trendyol/stove/testing/e2e/KtorApplicationUnderTestKt {
}

public final class com/trendyol/stove/testing/e2e/KtorBridgeSystem : com/trendyol/stove/testing/e2e/system/BridgeSystem, com/trendyol/stove/testing/e2e/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/testing/e2e/system/abstractions/PluggedSystem {
public fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;)V
public fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;)V
public fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;
public fun getTestSystem ()Lcom/trendyol/stove/testing/e2e/system/TestSystem;
}

public final class com/trendyol/stove/testing/e2e/KtorBridgeSystemKt {
public static final fun bridge-hQma78k (Lcom/trendyol/stove/testing/e2e/system/TestSystem;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;
public static final fun bridge-PmNtuJU (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;
public static synthetic fun bridge-PmNtuJU$default (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;
}

public final class com/trendyol/stove/testing/e2e/KtorDiCheck {
public static final field INSTANCE Lcom/trendyol/stove/testing/e2e/KtorDiCheck;
public final fun isKoinAvailable ()Z
public final fun isKtorDiAvailable ()Z
}

5 changes: 4 additions & 1 deletion starters/ktor/stove-ktor-testing-e2e/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
dependencies {
api(projects.lib.stoveTestingE2e)
implementation(libs.ktor.server.host.common)
implementation(libs.koin.ktor)

// Both DI systems as compileOnly - users bring their preferred DI at runtime
compileOnly(libs.koin.ktor)
compileOnly(libs.ktor.server.di)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.trendyol.stove.testing.e2e

import io.ktor.server.application.*
import io.ktor.server.plugins.di.*
import io.ktor.util.reflect.*
import org.koin.ktor.ext.getKoin
import kotlin.reflect.KClass
import kotlin.reflect.full.starProjectedType

/**
* Type alias for a dependency resolver function.
* Takes an Application and a KClass, returns the resolved dependency.
*/
typealias DependencyResolver = (Application, KClass<*>) -> Any

/**
* Default resolver implementations for supported DI frameworks.
*/
object DependencyResolvers {
/**
* Resolver for Koin DI framework.
*/
val koin: DependencyResolver = { application, klass -> application.getKoin().get(klass) }

/**
* Resolver for Ktor-DI framework.
*/
val ktorDi: DependencyResolver = { application, klass ->
require(application.attributes.contains(DependencyRegistryKey)) {
"Ktor-DI not installed in application. Make sure to install(DI) { ... } in your application."
}
val typeInfo = TypeInfo(klass, klass.starProjectedType)
application.dependencies.getBlocking(DependencyKey(type = typeInfo))
}

/**
* Auto-detects and returns the appropriate resolver based on available DI frameworks.
* Prefers Ktor-DI over Koin if both are available.
* Detection is deferred to runtime to ensure classpath is fully resolved.
*/
fun autoDetect(): DependencyResolver = { application, klass ->
val resolver = when {
KtorDiCheck.isKtorDiAvailable() -> ktorDi

KtorDiCheck.isKoinAvailable() -> koin

else -> error(
"No supported DI framework found. " +
"Add either Koin (io.insert-koin:koin-ktor) or Ktor-DI (io.ktor:ktor-server-di) to your classpath, " +
"or provide a custom resolver via bridge(resolver = { app, klass -> ... })"
)
}
resolver(application, klass)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,47 @@ import com.trendyol.stove.testing.e2e.system.*
import com.trendyol.stove.testing.e2e.system.abstractions.*
import com.trendyol.stove.testing.e2e.system.annotations.StoveDsl
import io.ktor.server.application.*
import org.koin.ktor.ext.*
import kotlin.reflect.KClass

/**
* A system that provides a bridge between the test system and the application context.
* Supports Koin, Ktor-DI, or a custom resolver for dependency resolution.
*
* @property testSystem the test system to bridge.
* @property resolver the dependency resolver function to use.
*/
@StoveDsl
class KtorBridgeSystem(
override val testSystem: TestSystem
override val testSystem: TestSystem,
private val resolver: DependencyResolver
) : BridgeSystem<Application>(testSystem),
PluggedSystem,
AfterRunAwareWithContext<Application> {
override fun <D : Any> get(klass: KClass<D>): D = ctx.getKoin().get(klass)
@Suppress("UNCHECKED_CAST")
override operator fun <D : Any> get(klass: KClass<D>): D = resolver(ctx, klass) as D
}

/**
* Registers the Ktor bridge system with automatic DI detection or a custom resolver.
* Supports Koin and Ktor-DI out of the box.
*
* Example usage with auto-detect:
* ```kotlin
* bridge() // Auto-detects Koin or Ktor-DI
* ```
*
* Example usage with custom resolver:
* ```kotlin
* bridge { application, klass ->
* application.myCustomDi.resolve(klass)
* }
* ```
*
* @param resolver a function that takes an Application and KClass and returns the resolved dependency.
* Defaults to auto-detecting Koin or Ktor-DI.
* @throws IllegalStateException if no DI framework is available and no custom resolver is provided.
*/
@StoveDsl
fun WithDsl.bridge(): TestSystem = this.testSystem.withBridgeSystem(KtorBridgeSystem(this.testSystem))
fun WithDsl.bridge(
resolver: DependencyResolver = DependencyResolvers.autoDetect()
): TestSystem = this.testSystem.withBridgeSystem(KtorBridgeSystem(this.testSystem, resolver))
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@file:Suppress("TooGenericExceptionCaught", "SwallowedException")

package com.trendyol.stove.testing.e2e

/**
* Checks which DI system is available on the classpath.
*/
object KtorDiCheck {
/**
* Returns true if Koin is available on the classpath.
*/
fun isKoinAvailable(): Boolean = try {
Class.forName("org.koin.ktor.ext.ApplicationExtKt")
true
} catch (_: ClassNotFoundException) {
false
}

/**
* Returns true if Ktor-DI is available on the classpath.
*/
fun isKtorDiAvailable(): Boolean = try {
Class.forName("io.ktor.server.plugins.di.DependencyInjectionConfig")
true
} catch (_: ClassNotFoundException) {
false
}
}
Empty file.
15 changes: 15 additions & 0 deletions starters/ktor/tests/ktor-di-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
dependencies {
api(projects.starters.ktor.stoveKtorTestingE2e)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.di)
testImplementation(testFixtures(projects.starters.ktor.tests.ktorTestFixtures))
}

dependencies {
testImplementation(libs.slf4j.simple)
}

tasks.test.configure {
systemProperty("kotest.framework.config.fqn", "com.trendyol.stove.testing.e2e.ktor.KtorDiStove")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.trendyol.stove.testing.e2e.ktor

import com.trendyol.stove.testing.e2e.bridge
import com.trendyol.stove.testing.e2e.ktor
import com.trendyol.stove.testing.e2e.system.TestSystem
import io.kotest.core.config.AbstractProjectConfig

class KtorDiStove : AbstractProjectConfig() {
override suspend fun beforeProject(): Unit =
TestSystem()
.with {
bridge() // Auto-detects Ktor-DI
ktor(
runner = { params ->
KtorDiTestApp.run(params)
}
)
}.run()

override suspend fun afterProject(): Unit = TestSystem.stop()
}

class KtorDiBridgeSystemTests : BridgeSystemTests(KtorDiStove())
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.trendyol.stove.testing.e2e.ktor

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.di.*
import org.junit.platform.commons.logging.LoggerFactory
import java.net.ServerSocket

/**
* Test Ktor application using Ktor-DI for dependency injection.
*/
object KtorDiTestApp {
private val logger = LoggerFactory.getLogger(KtorDiTestApp::class.java)

fun run(args: Array<String>): Application {
logger.info { "Starting Ktor-DI test application with args: ${args.joinToString(" ")}" }
val port = findAvailablePort()
val applicationEngine = embeddedServer(Netty, port = port, host = "localhost") {
dependencies {
provide<GetUtcNow> { SystemTimeGetUtcNow() }
provide<ExampleService> { ExampleService(resolve()) }
provide<TestConfig> { TestConfig() }
}
}
applicationEngine.start(wait = false)
return applicationEngine.application
}

private fun findAvailablePort(): Int = ServerSocket(0).use { it.localPort }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
org.slf4j.simpleLogger.defaultLogLevel=info
org.slf4j.simpleLogger.showDateTime=true
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss.SSS
org.slf4j.simpleLogger.showShortLogName=true

Empty file.
15 changes: 15 additions & 0 deletions starters/ktor/tests/ktor-koin-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
dependencies {
api(projects.starters.ktor.stoveKtorTestingE2e)
implementation(libs.ktor.server.netty)
implementation(libs.koin.ktor)
testImplementation(testFixtures(projects.starters.ktor.tests.ktorTestFixtures))
}

dependencies {
testImplementation(libs.slf4j.simple)
}

tasks.test.configure {
systemProperty("kotest.framework.config.fqn", "com.trendyol.stove.testing.e2e.ktor.KoinStove")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.trendyol.stove.testing.e2e.ktor

import com.trendyol.stove.testing.e2e.bridge
import com.trendyol.stove.testing.e2e.ktor
import com.trendyol.stove.testing.e2e.system.TestSystem
import io.kotest.core.config.AbstractProjectConfig

class KoinStove : AbstractProjectConfig() {
override suspend fun beforeProject(): Unit =
TestSystem()
.with {
bridge() // Auto-detects Koin
ktor(
runner = { params ->
KoinTestApp.run(params)
}
)
}.run()

override suspend fun afterProject(): Unit = TestSystem.stop()
}

class KoinBridgeSystemTests : BridgeSystemTests(KoinStove())
Loading
Loading