Skip to content

Getting Started

Boot the application under test with the dependencies it actually talks to. These five steps show the minimum wiring, where Stove takes over the runtime lifecycle, and how to verify the setup before adding more systems.

Terminology used below: AUT means application under test; a system is a Stove module registered in Stove().with { ... }, such as HTTP, PostgreSQL, Kafka, WireMock, tracing, or dashboard; an AUT runner registers how Stove starts or targets the app; provided means Stove connects to existing infrastructure instead of starting a Testcontainer; bridge means optional DI-container access for supported JVM frameworks.

If you'd rather click The Setup Wizard generates the dependency block, StoveConfig.kt, and a runnable sample test from your selections. This page is the manual path and explains the lifecycle behind the generated code.

Prerequisites

JDK 17+

Required for Stove and all starters.

🐳
Docker

For Testcontainers (default). Skip if you use Provided Instances.

📦
Gradle (recommended)

Examples use Gradle Kotlin DSL. Maven works for deps; the stoveTracing plugin needs Gradle.

🧪
Kotest 6.1.3+ or JUnit Jupiter 6.x

Either test framework. Kotest gets first-class wiring.

IDE setup

IntelliJ IDEA + the Kotest plugin = run buttons on every test {} block. Worth installing on day one.

The five steps

  1. deps

    Add the minimum dependencies

    Start with the smallest set that proves the wiring works: BOM + core + one AUT runner + one test extension + stove-http. Add database, messaging, mocks, and observability modules only when the app actually uses them.

    Gradle recommended

    All examples use Gradle (Kotlin DSL). Maven works for Stove dependencies; the stoveTracing Gradle plugin is the easiest path to OTel tracing and is the recommended setup for observability.

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

    Version alignment

    Keep the BOM, every com.trendyol:stove-*, and stove-cli (if you use the dashboard) on the same version. Check Releases.

    Systems are á-la-carte. Each dependency adds one DSL block and, when needed, one options object for container/provided runtime configuration:

    Module Use for
    stove-kafka, stove-spring-kafka event flows
    stove-postgres, stove-mysql, stove-mssql, stove-mongodb, stove-couchbase, stove-cassandra, stove-redis, stove-elasticsearch persistence
    stove-wiremock, stove-grpc-mock external surface mocks
    stove-grpc gRPC client
    stove-tracing, stove-dashboard observability

    Enable tracing with the Gradle plugin

    stove-tracing is wired by the com.trendyol.stove.tracing Gradle plugin. For in-process JVM applications launched by Stove, it attaches the OpenTelemetry Java agent to your test JVM, starts an OTLP gRPC receiver, and exposes the endpoint to your AUT without application-code changes.

    plugins { id("com.trendyol.stove.tracing") version "$stoveVersion" }
    
    stoveTracing {
        serviceName.set("my-service")
        testTaskNames.set(listOf("e2eTest"))
    }
    

    Failures then come with a full call chain inside your app. See Tracing and When a test fails.

  2. app

    Expose a reusable entrypoint

    Stove boots your app from tests. Extract startup into a reusable run(args) function so production main and the Stove runner execute the same startup path.

    @SpringBootApplication
    class MyApplication
    
    fun main(args: Array<String>) = run(args)
    
    fun run(
        args: Array<String>,
        init: SpringApplication.() -> Unit = {}
    ): ConfigurableApplicationContext =
        runApplication<MyApplication>(*args, init = init)
    
    object MyApp {
        @JvmStatic fun main(args: Array<String>) = run(args)
    
        fun run(
            args: Array<String>,
            wait: Boolean = true,
            testModules: List<Module> = emptyList()
        ): Application = embeddedServer(Netty, port = args.getPort()) {
            install(Koin) { modules(appModule, *testModules.toTypedArray()) }
            configureRouting()
        }.start(wait = wait).application
    }
    
    object MyApp {
        @JvmStatic fun main(args: Array<String>) = run(args)
    
        fun run(
            args: Array<String>,
            wait: Boolean = true,
            testDependencies: (DependencyRegistrar.() -> Unit)? = null
        ): Application = embeddedServer(Netty, port = args.getPort()) {
            install(DI) {
                dependencies {
                    provide<MyService> { MyServiceImpl() }
                    testDependencies?.invoke(this)
                }
            }
            configureRouting()
        }.start(wait = wait).application
    }
    
    fun main(args: Array<String>) = run(args)
    
    fun run(
        args: Array<String>,
        init: ApplicationContext.() -> Unit = {}
    ): ApplicationContext = ApplicationContext.builder()
        .args(*args)
        .build()
        .also(init)
        .start()
        .also { ctx ->
            ctx.findBean(EmbeddedApplication::class.java).ifPresent { app ->
                if (!app.isRunning) app.start()
            }
        }
    
    @QuarkusMain
    object QuarkusMainApp {
        @JvmStatic fun main(args: Array<String>) { Quarkus.run(*args) }
    }
    
    @ApplicationScoped
    class StoveStartupSignal {
        fun onStart(@Observes event: StartupEvent) =
            System.setProperty("stove.quarkus.ready", "true")
        fun onStop(@Observes event: ShutdownEvent) =
            System.clearProperty("stove.quarkus.ready")
    }
    

    Quarkus needs a startup signal if your app has no HTTP endpoint Stove can probe. See the Quarkus guide for full details.

  3. config

    Configure Stove once per suite

    Put e2e tests in a dedicated src/test-e2e/ source set (why). AbstractProjectConfig.beforeProject() runs once for the entire suite. Register the systems your app talks to, then register one AUT runner last, and tear Stove down in afterProject().

    At runtime, Stove().with { ... }.run() starts registered systems, collects their exposed configuration, starts the application under test with those properties/arguments, and leaves the suite ready for test bodies to call stove { ... }. Stove.stop() in afterProject() tears down the application and systems.

    System-derived config vs static runner parameters

    Use configureExposedConfiguration for values Stove only knows at runtime: container host/port, generated credentials, WireMock base URL, Kafka bootstrap servers, and interceptor classes. Use withParameters for static application settings such as server.port=8080 or logging levels. This applies to framework/process/container runners. A providedApplication() target is already running, so Stove cannot inject environment variables, CLI args, or application properties into it; configure that app externally before the suite starts.

    Kotest 6.x discovery

    AbstractProjectConfig is not auto-scanned in Kotest 6.x. Add src/test-e2e/resources/kotest.properties:

    kotest.framework.config.fqn=com.myapp.e2e.TestConfig
    

    class TestConfig : AbstractProjectConfig() {
        override val extensions: List<Extension> = listOf(StoveKotestExtension())
    
        override suspend fun beforeProject() {
            Stove().with {
                httpClient {
                    HttpClientSystemOptions(baseUrl = "http://localhost:8080")
                }
    
                // Swap springBoot for ktor / micronaut / quarkus
                springBoot(
                    runner = { params -> com.myapp.run(params) },
                    withParameters = listOf("server.port=8080", "logging.level.root=warn")
                )
            }.run()
        }
    
        override suspend fun afterProject() = Stove.stop()
    }
    
    @ExtendWith(StoveJUnitExtension::class)
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    abstract class BaseE2ETest {
        companion object {
            @JvmStatic @BeforeAll
            fun setup() = runBlocking {
                Stove().with {
                    httpClient { HttpClientSystemOptions(baseUrl = "http://localhost:8080") }
                    springBoot(
                        runner = { params -> com.myapp.run(params) },
                        withParameters = listOf("server.port=8080")
                    )
                }.run()
            }
    
            @JvmStatic @AfterAll
            fun teardown() = runBlocking { Stove.stop() }
        }
    }
    
  4. first test

    Write the first assertion

    The DSL is stove { http { ... } }. The block uses the HTTP system registered during suite startup, sends the request to the application under test, and gives you the decoded response for assertions.

    class MyFirstE2ETest : FunSpec({
      test("GET /hello returns greeting") {
        stove {
          http {
            get<String>("/hello") { body ->
              body shouldBe "Hello, World!"
            }
          }
        }
      }
    })
    
    class MyFirstE2ETest : BaseE2ETest() {
      @Test
      fun `GET hello returns greeting`() = runBlocking {
        stove {
          http {
            get<String>("/hello") { body -> body shouldBe "Hello, World!" }
          }
        }
      }
    }
    

    Run it:

    ./gradlew e2eTest                                # all e2e tests
    ./gradlew e2eTest --tests "com.myapp.e2e.*Test"  # filter
    
  5. grow

    Add the systems your app actually uses

    The same .with { } block composes more systems. Each system can expose runtime values such as host, port, credentials, or interceptor classes; the runner receives those values before the app boots. Below: HTTP in, Kafka events, Couchbase persistence, WireMock for outbound calls, and bridge() for DI access on supported JVM frameworks.

    Stove().with {
        httpClient { HttpClientSystemOptions(baseUrl = "http://localhost:8080") }
    
        kafka {
            KafkaSystemOptions { cfg -> listOf(
                "kafka.bootstrapServers=${cfg.bootstrapServers}",
                "kafka.interceptorClasses=${cfg.interceptorClass}"
            ) }
        }
    
        couchbase {
            CouchbaseSystemOptions(
                defaultBucket = "myBucket",
                configureExposedConfiguration = { cfg -> listOf(
                    "couchbase.hosts=${cfg.hostsWithPort}",
                    "couchbase.username=${cfg.username}",
                    "couchbase.password=${cfg.password}"
                ) }
            )
        }
    
        wiremock {
            WireMockSystemOptions(
                port = 0,
                configureExposedConfiguration = { cfg ->
                    listOf("external.service.url=${cfg.baseUrl}")
                }
            )
        }
        bridge()  // DI access for setup + verification
    
        springBoot(
            runner = { params -> com.myapp.run(params) },
            withParameters = listOf("server.port=8080")
        )
    }.run()
    

    Then assert across systems in one test:

    test("creating an order persists, calls payment, and publishes event") {
      stove {
        val orderId = UUID.randomUUID().toString()
    
        wiremock { mockPost("/payments", 200, PaymentResult(true).some()) }
    
        http {
          postAndExpectBody<OrderResponse>(
            uri = "/orders",
            body = CreateOrderRequest(orderId, listOf("item1", "item2"), 99.99).some()
          ) { it.status shouldBe 201 }
        }
    
        couchbase {
          shouldGet<Order>("orders", orderId) { order ->
            order.status shouldBe "CREATED"
            order.amount shouldBe 99.99
          }
        }
    
        kafka {
          shouldBePublished<OrderCreatedEvent> {
            actual.orderId == orderId && actual.amount == 99.99
          }
        }
    
        using<OrderService> { getOrder(orderId).status shouldBe "CREATED" }
      }
    }
    

    One DSL, every surface that matters.

Do this, not that

Generate unique IDs per test run.
val userId = "user-${UUID.randomUUID()}"
No collisions, parallel-safe, and compatible with shared infrastructure.
Hard-code identifiers.
val userId = "user-1"  // collides on re-run
Use Stove's time-bounded assertions instead of sleeping.
shouldBePublished<E> {
  actual.userId == userId
}
Block the thread.
Thread.sleep(5_000)  // flaky, slow
kafka { shouldBePublished<E>(...) }
Configure Stove **once** per suite so containers, clients, and the application are reused for all tests in that suite.
override suspend fun beforeProject() = Stove().with { ... }.run()
Start Stove per test.
@BeforeEach fun setup() = Stove().with { ... }.run()  // very slow

Local-loop optimizations

Keep containers running between local runs during development:

Stove {
    keepDependenciesRunning()
}.with { /* ... */ }.run()

Custom registry (firewalls, private mirrors):

DEFAULT_REGISTRY = "your.registry.com"  // global

kafka {                                  // per component
    KafkaSystemOptions(
        container = KafkaContainerOptions(registry = "your.registry.com")
    )
}

Troubleshooting at a glance

Symptom Fix
Docker not found Start Docker Desktop / colima
Port conflicts Use port 0 for mocks; let Stove pick
Slow startup keepDependenciesRunning() during local development
Serialization errors Align StoveSerde with your app's mapper
Tests collide Generate unique IDs per test
Kafka assertion times out Use test-friendly Kafka settings

Deeper troubleshooting guide and best practices.

Where to go next