Getting Started¶
Get Stove running in your project in just a few minutes. Stove helps you write end-to-end tests by spinning up your application and all its dependencies (databases, message queues, etc.) together, so you can test the real thing instead of mocks.
If you already know your application framework, the quickest route is Supported Frameworks. This guide focuses on the shared setup that applies across all starters.
What You'll Need¶
Make sure you have these installed:
- JDK 17+ - Stove needs Java 17 or higher
- Docker - Get the latest version (Stove uses testcontainers, so Docker is required)
- Kotlin 1.8+ - For writing your tests
- Gradle or Maven - We use Gradle in all examples, but Maven works too
IDE Setup
If you're using IntelliJ IDEA, grab the Kotest plugin. It adds run buttons and makes test discovery much smoother.
Fastest Path¶
If you want the smallest useful Stove setup, do this first:
- Add
stove, one starter, one test extension, andstove-http. - Expose a reusable application entrypoint that Stove can call.
- Start Stove once for the suite.
- Make one real HTTP request and assert the result.
Everything else can be added incrementally.
Step 1: Add The Minimum Dependencies¶
Start with the smallest set that proves the flow works:
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("com.trendyol:stove-bom:$stoveVersion"))
testImplementation("com.trendyol:stove")
// Pick one starter
testImplementation("com.trendyol:stove-spring")
// Pick one test framework extension
testImplementation("com.trendyol:stove-extensions-kotest")
// Start with one public surface
testImplementation("com.trendyol:stove-http")
}
Latest Version
Check the Releases page for the latest version.
Replace stove-spring with the starter that matches your runtime:
- Spring Boot:
stove-spring - Ktor:
stove-ktor - Micronaut:
stove-micronaut - Quarkus:
stove-quarkus
Then add only the components you actually need:
stove-httpfor REST APIsstove-kafkafor event flowsstove-postgres,stove-mysql,stove-mongodb, orstove-redisfor persistencestove-wiremockorstove-grpc-mockfor external dependenciesstove-tracingfor richer diagnostics
If you are using Ktor, also add your preferred DI support. See the Ktor guide for the exact setup.
Step 2: Prepare Your Application¶
Stove needs to start your application from tests, which means your app needs a reusable entrypoint. The shared pattern is:
- keep the normal
main - move the actual startup logic into a reusable
run(args)style function - pass that function to Stove as the
runner
You only need the version for your framework:
// Before
@SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
// After
@SpringBootApplication
class MyApplication
fun main(args: Array<String>) = run(args)
fun run(
args: Array<String>,
init: SpringApplication.() -> Unit = {}
): ConfigurableApplicationContext {
return runApplication<MyApplication>(*args, init = init)
}
// Before
fun main() {
embeddedServer(Netty, port = 8080) {
install(Koin) { modules(appModule) }
configureRouting()
}.start(wait = true)
}
// After - Accept test modules for overriding beans
object MyApp {
@JvmStatic
fun main(args: Array<String>) = run(args)
fun run(
args: Array<String>,
wait: Boolean = true,
testModules: List<Module> = emptyList()
): Application {
return embeddedServer(Netty, port = args.getPort()) {
install(Koin) {
modules(appModule, *testModules.toTypedArray())
}
configureRouting()
}.start(wait = wait).application
}
}
// Before
fun main() {
embeddedServer(Netty, port = 8080) {
install(DI) { dependencies { provide<MyService> { MyServiceImpl() } } }
configureRouting()
}.start(wait = true)
}
// After - Accept test dependency overrides
object MyApp {
@JvmStatic
fun main(args: Array<String>) = run(args)
fun run(
args: Array<String>,
wait: Boolean = true,
testDependencies: (DependencyRegistrar.() -> Unit)? = null
): Application {
return embeddedServer(Netty, port = args.getPort()) {
install(DI) {
dependencies {
provide<MyService> { MyServiceImpl() }
testDependencies?.invoke(this) // Apply test overrides
}
}
configureRouting()
}.start(wait = wait).application
}
}
fun main(args: Array<String>) {
run(args)
}
fun run(
args: Array<String>,
init: ApplicationContext.() -> Unit = {}
): ApplicationContext {
val context = ApplicationContext
.builder()
.args(*args)
.build()
.also(init)
.start()
context.findBean(EmbeddedApplication::class.java).ifPresent { app ->
if (!app.isRunning) {
app.start()
}
}
return context
}
package com.example
import io.quarkus.runtime.Quarkus
import io.quarkus.runtime.ShutdownEvent
import io.quarkus.runtime.StartupEvent
import io.quarkus.runtime.annotations.QuarkusMain
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.event.Observes
@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")
}
}
Stove calls your Quarkus main function directly. If your app has no HTTP endpoint, publish a startup signal like the one above so Stove can detect readiness. See the Quarkus guide for the full setup, including Kafka and tracing notes.
Step 3: Create Test Configuration¶
Set up Stove once for your entire test suite. This configuration runs before all your tests and shuts down after they're done. Use Stove() and .with { } to configure your test environment.
If you are aiming for the fastest first success, start with one starter plus stove-http, confirm the app boots and responds, and only then add Kafka, databases, tracing, or mocks.
We recommend putting e2e tests in a separate src/test-e2e source set to keep them separate from unit tests (see Best Practices for the Gradle setup).
Test Framework Extensions
StoveKotestExtension and StoveJUnitExtension are separate packages that must be on your classpath:
testImplementation("com.trendyol:stove-extensions-kotest") // For Kotest
// or
testImplementation("com.trendyol:stove-extensions-junit") // For JUnit
Kotest requires 6.1.3 or later. JUnit requires Jupiter 6.x if possible. In Kotest 6.x, AbstractProjectConfig is no longer auto-scanned. Create a kotest.properties file in your test resources (e.g. src/test-e2e/resources/kotest.properties):
Set the value to the fully qualified name of your AbstractProjectConfig class.
If you are testing a Quarkus application, see the Quarkus guide for the starter-specific setup and limitations.
// src/test-e2e/kotlin/io/kotest/provided/ProjectConfig.kt
import com.trendyol.stove.extensions.kotest.StoveKotestExtension
import com.trendyol.stove.system.Stove
import com.trendyol.stove.system.stove
import com.trendyol.stove.http.*
import com.trendyol.stove.spring.springBoot
class TestConfig : AbstractProjectConfig() {
// Optional: Add this for detailed failure reports with execution context
override val extensions: List<Extension> = listOf(StoveKotestExtension())
override suspend fun beforeProject() {
Stove()
.with {
httpClient {
HttpClientSystemOptions(
baseUrl = "http://localhost:8080"
)
}
// Replace `springBoot` with `ktor`, `micronaut`, or `quarkus` as needed
springBoot(
runner = { params ->
com.myapp.run(params)
},
withParameters = listOf(
"server.port=8080",
"logging.level.root=warn"
)
)
}
.run()
}
override suspend fun afterProject() {
Stove.stop()
}
}
// src/test-e2e/kotlin/e2e/TestConfig.kt
import com.trendyol.stove.extensions.junit.StoveJUnitExtension
import com.trendyol.stove.system.Stove
import com.trendyol.stove.http.*
import com.trendyol.stove.spring.springBoot
import org.junit.jupiter.api.extension.ExtendWith
// Optional: Add this annotation for detailed failure reports
@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"
)
}
// Replace `springBoot` with `ktor`, `micronaut`, or `quarkus` as needed
springBoot(
runner = { params ->
com.myapp.run(params)
},
withParameters = listOf(
"server.port=8080",
"logging.level.root=warn"
)
)
}
.run()
}
@JvmStatic
@AfterAll
fun teardown() = runBlocking {
Stove.stop()
}
}
}
Step 4: Write Your First Test¶
import com.trendyol.stove.system.stove
class MyFirstE2ETest : FunSpec({
test("should return hello world") {
stove {
http {
get<String>("/hello") { response ->
response shouldBe "Hello, World!"
}
}
}
}
test("should create a user") {
stove {
http {
postAndExpectBody<UserResponse>(
uri = "/users",
body = CreateUserRequest(name = "John", email = "john@example.com").some()
) { response ->
response.status shouldBe 201
response.body().name shouldBe "John"
}
}
}
}
})
import com.trendyol.stove.system.stove
class MyFirstE2ETest : BaseE2ETest() {
@Test
fun `should return hello world`() = runBlocking {
stove {
http {
get<String>("/hello") { response ->
response shouldBe "Hello, World!"
}
}
}
}
@Test
fun `should create a user`() = runBlocking {
stove {
http {
postAndExpectBody<UserResponse>(
uri = "/users",
body = CreateUserRequest(name = "John", email = "john@example.com").some()
) { response ->
response.status shouldBe 201
response.body().name shouldBe "John"
}
}
}
}
}
Step 5: Add More Components¶
Once you've got the basics working, you'll probably want to add more components. Here's how you'd set up a typical stack:
Stove()
.with {
httpClient {
HttpClientSystemOptions(baseUrl = "http://localhost:8080")
}
// Add Kafka for event-driven tests
kafka {
KafkaSystemOptions {
listOf(
"kafka.bootstrapServers=${it.bootstrapServers}",
"kafka.interceptorClasses=${it.interceptorClass}"
)
}
}
// Add Couchbase for database tests
couchbase {
CouchbaseSystemOptions(
defaultBucket = "myBucket",
configureExposedConfiguration = { cfg ->
listOf(
"couchbase.hosts=${cfg.hostsWithPort}",
"couchbase.username=${cfg.username}",
"couchbase.password=${cfg.password}"
)
}
)
}
// Add WireMock for external service mocking
wiremock {
WireMockSystemOptions(port = 9090)
}
// Add bridge for DI container access
bridge()
springBoot(
runner = { params -> com.myapp.run(params) },
withParameters = listOf(
"server.port=8080",
"external.service.url=http://localhost:9090"
)
)
}
.run()
Step 6: Write Tests That Span Multiple Systems¶
Here's where Stove really shines. You can write tests that touch multiple systems and verify everything works together:
import com.trendyol.stove.system.stove
test("should create order and publish event") {
stove {
val orderId = UUID.randomUUID().toString()
// Mock external payment service
wiremock {
mockPost(
url = "/payments",
statusCode = 200,
responseBody = PaymentResult(success = true).some()
)
}
// Create order via API
http {
postAndExpectBody<OrderResponse>(
uri = "/orders",
body = CreateOrderRequest(
id = orderId,
items = listOf("item1", "item2"),
amount = 99.99
).some()
) { response ->
response.status shouldBe 201
}
}
// Verify order stored in database
couchbase {
shouldGet<Order>("orders", orderId) { order ->
order.status shouldBe "CREATED"
order.amount shouldBe 99.99
}
}
// Verify event was published
kafka {
shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {
actual.orderId == orderId &&
actual.amount == 99.99
}
}
// Access application beans directly
using<OrderService> {
val order = getOrder(orderId)
order.status shouldBe "CREATED"
}
}
}
Stove starts your application with its dependencies, runs your tests, and shuts everything down when done.
Running Tests¶
Run all your tests:
Or run a specific test class:
If you're using the test-e2e source set, you might have a separate task:
Next Steps¶
Now that you're up and running, here's what to explore next:
- Components - Check out the Components documentation to see what's available
- Quarkus - If your application uses Quarkus, follow the Quarkus guide
- Tracing - Enable Tracing to see exactly what happened inside your application when a test fails
- Reporting - Set up Reporting to get detailed failure diagnostics
- gRPC Mocking - Mock external gRPC services with gRPC Mocking
- Best Practices - Read the Best Practices guide for tips on writing effective e2e tests
- Troubleshooting - Hit an issue? Check the Troubleshooting guide
- Examples - Browse the Examples and Recipes for complete working projects
Common Patterns¶
Keep Containers Running Between Test Runs¶
Starting containers takes time. During development, you can keep them running between test runs to speed things up:
Using a Custom Container Registry¶
If you're behind a corporate firewall or need to use a private registry:
// Set globally
DEFAULT_REGISTRY = "your.registry.com"
// Or per component
kafka {
KafkaSystemOptions(
container = KafkaContainerOptions(
registry = "your.registry.com"
)
)
}
Use Unique Test Data¶
To avoid test conflicts, generate unique data for each test run:
test("should create user") {
val userId = UUID.randomUUID().toString()
val email = "test-${UUID.randomUUID()}@example.com"
stove {
// Use unique data to avoid conflicts
}
}
Troubleshooting Quick Tips¶
| Problem | Solution |
|---|---|
| Docker not found | Ensure Docker is running and accessible |
| Port conflicts | Use dynamic ports or ensure no conflicts |
| Slow startup | Enable keepDependenciesRunning() for development |
| Serialization errors | Configure StoveSerde to match your app's serializer |
| Test isolation issues | Use unique test data and cleanup functions |
For more help, see the Troubleshooting Guide.