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¶
Required for Stove and all starters.
For Testcontainers (default). Skip if you use Provided Instances.
Examples use Gradle Kotlin DSL. Maven works for deps; the stoveTracing plugin needs Gradle.
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¶
-
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
stoveTracingGradle 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-*, andstove-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-kafkaevent flows stove-postgres,stove-mysql,stove-mssql,stove-mongodb,stove-couchbase,stove-cassandra,stove-redis,stove-elasticsearchpersistence stove-wiremock,stove-grpc-mockexternal surface mocks stove-grpcgRPC client stove-tracing,stove-dashboardobservability Enable tracing with the Gradle plugin
stove-tracingis wired by thecom.trendyol.stove.tracingGradle 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.
-
app
Expose a reusable entrypoint¶
Stove boots your app from tests. Extract startup into a reusable
run(args)function so productionmainand the Stove runner execute the same startup path.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.
-
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 inafterProject().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 callstove { ... }.Stove.stop()inafterProject()tears down the application and systems.System-derived config vs static runner parameters
Use
configureExposedConfigurationfor values Stove only knows at runtime: container host/port, generated credentials, WireMock base URL, Kafka bootstrap servers, and interceptor classes. UsewithParametersfor static application settings such asserver.port=8080or logging levels. This applies to framework/process/container runners. AprovidedApplication()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
AbstractProjectConfigis not auto-scanned in Kotest 6.x. Addsrc/test-e2e/resources/kotest.properties: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() } } } -
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.Run it:
-
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, andbridge()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¶
Local-loop optimizations¶
Keep containers running between local runs during development:
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¶
-
Wizard for your stack · Open wizard
-
Real flows · Recipes
-
Per-framework setup · Frameworks
-
System reference · Components
-
When a test fails · Observability story
-
Working examples · GitHub