Skip to content

Getting Started

This guide will help you get Stove up and running in your project in just a few minutes.

Prerequisites

Before you begin, ensure you have:

  • JDK 17+ - Stove requires Java 17 or higher
  • Docker - Latest version recommended (Stove uses testcontainers under the hood)
  • Kotlin 1.8+ - For writing your tests
  • Gradle or Maven - Gradle is recommended and used in all examples

IDE Setup

If you're using IntelliJ IDEA, install the Kotest plugin for a better testing experience with run buttons and test discovery.

Step 1: Add Dependencies

Add Stove to your build.gradle.kts:

repositories {
    mavenCentral()
}

dependencies {
    // Core framework
    testImplementation("com.trendyol:stove-testing-e2e:$stoveVersion")

    // Choose your application framework
    testImplementation("com.trendyol:stove-spring-testing-e2e:$stoveVersion")
    // OR
    testImplementation("com.trendyol:stove-ktor-testing-e2e:$stoveVersion")

    // Add components you need
    testImplementation("com.trendyol:stove-testing-e2e-http:$stoveVersion")
    testImplementation("com.trendyol:stove-testing-e2e-kafka:$stoveVersion")
    // ... add more as needed
}

Latest Version

Check the Releases page for the latest version.

Step 2: Prepare Your Application

Stove needs to start your application from the test context. This requires a small modification to your main function.

// 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) {
        configureRouting()
    }.start(wait = true)
}

// After
object MyApp {
    @JvmStatic
    fun main(args: Array<String>) = run(args)

    fun run(
        args: Array<String>,
        wait: Boolean = true,
        configure: Application.() -> Unit = {}
    ): Application {
        // Your application setup
        return embeddedServer(Netty, port = args.getPort()) {
            configureRouting()
            configure()
        }.start(wait = wait)
    }
}

Step 3: Create Test Configuration

Set up Stove once for your entire test suite. We recommend using a dedicated src/test-e2e source set for e2e tests (see Best Practices for Gradle configuration).

// src/test-e2e/kotlin/e2e/TestConfig.kt
class TestConfig : AbstractProjectConfig() {

    override suspend fun beforeProject() {
        TestSystem()
            .with {
                httpClient {
                    HttpClientSystemOptions(
                        baseUrl = "http://localhost:8080"
                    )
                }

                springBoot(
                    runner = { params -> 
                        com.myapp.run(params)
                    },
                    withParameters = listOf(
                        "server.port=8080",
                        "logging.level.root=warn"
                    )
                )
            }
            .run()
    }

    override suspend fun afterProject() {
        TestSystem.stop()
    }
}
// src/test-e2e/kotlin/e2e/TestConfig.kt
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BaseE2ETest {

    companion object {
        @JvmStatic
        @BeforeAll
        fun setup() = runBlocking {
            TestSystem()
                .with {
                    httpClient {
                        HttpClientSystemOptions(
                            baseUrl = "http://localhost:8080"
                        )
                    }

                    springBoot(
                        runner = { params -> 
                            com.myapp.run(params)
                        },
                        withParameters = listOf(
                            "server.port=8080",
                            "logging.level.root=warn"
                        )
                    )
                }
                .run()
        }

        @JvmStatic
        @AfterAll
        fun teardown() = runBlocking {
            TestSystem.stop()
        }
    }
}

Step 4: Write Your First Test

class MyFirstE2ETest : FunSpec({

    test("should return hello world") {
        TestSystem.validate {
            http {
                get<String>("/hello") { response ->
                    response shouldBe "Hello, World!"
                }
            }
        }
    }

    test("should create a user") {
        TestSystem.validate {
            http {
                postAndExpectBody<UserResponse>(
                    uri = "/users",
                    body = CreateUserRequest(name = "John", email = "john@example.com").some()
                ) { response ->
                    response.status shouldBe 201
                    response.body().name shouldBe "John"
                }
            }
        }
    }
})
class MyFirstE2ETest : BaseE2ETest() {

    @Test
    fun `should return hello world`() = runBlocking {
        TestSystem.validate {
            http {
                get<String>("/hello") { response ->
                    response shouldBe "Hello, World!"
                }
            }
        }
    }

    @Test
    fun `should create a user`() = runBlocking {
        TestSystem.validate {
            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

As your application grows, add more components:

TestSystem()
    .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 Comprehensive Tests

Now you can write tests that span multiple systems:

test("should create order and publish event") {
    TestSystem.validate {
        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"
        }
    }
}

Running Tests

Run your tests using Gradle:

./gradlew test

Or run specific test classes:

./gradlew test --tests "com.myapp.e2e.OrderE2ETest"

Next Steps

Common Patterns

Keep Dependencies Running

For faster development cycles, keep containers running between test runs:

TestSystem {
    keepDependenciesRunning()
}.with {
    // Your configuration
}.run()

Custom Container Registry

If you're behind a corporate firewall:

// Set globally
DEFAULT_REGISTRY = "your.registry.com"

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

Use Random Test Data

Generate unique data for each test:

test("should create user") {
    val userId = UUID.randomUUID().toString()
    val email = "test-${UUID.randomUUID()}@example.com"

    TestSystem.validate {
        // 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.