Stove
βWhere the infrastructure burns down to pure configurations(ashes)β
What is Stove?
Stove is an end-to-end testing framework that simplifies testing by managing physical dependencies and your application in a unified way. Write infrastructure-agnostic but component-aware tests in Kotlin, regardless of your JVM-based tech stack.
Key Features
- π Zero Boilerplate: Write clean, focused tests without infrastructure setup code
- π Pluggable Architecture: Easily extend with custom infrastructure components
- π³ Docker-Based: Leverages Testcontainers for reliable, isolated test environments
- π Framework Agnostic: Works with Spring, Ktor, Micronaut and other JVM frameworks (up for grabs)
- π Physical Dependencies: Built-in support for Kafka, Couchbase, PostgreSQL, and more
- β‘ Fast Development: On top of the boilerplate free testing, optional Reuse test containers for blazing-fast local development
Supported Infrastructure
Physical dependencies:
- β Kafka
- β Couchbase
- β PostgreSQL
- β ElasticSearch
- β MongoDB
- β MSSQL
- β Redis
- β HTTP Client
- β WireMock
Frameworks:
- β Spring
- β Ktor
- β Micronaut
- β Quarkus (incubating)
Quick Start
Add the dependency
// Add the following dependencies to your build.gradle.kts
testImplementation("com.trendyol:stove-testing-e2e:${version}")
// And the any of the following for the infrastructure you want to use, for example Kafka
// you can also use Couchbase, PostgreSQL, ElasticSearch, MongoDB, MSSQL, Redis, HTTP Client, WireMock
// as much as you want
testImplementation("com.trendyol:stove-testing-e2e-kafka:${version}")
// And Application Under Test (AUT)
testImplementation("com.trendyol:stove-ktor-testing-e2e:${version}")
// Or
testImplementation("com.trendyol:stove-spring-testing-e2e:${version}")
Set Up the TestSystem
TestSystem() {
if (isRunningLocally()) {
enableReuseForTestContainers()
// this will keep the dependencies running
// after the tests are finished,
// so next run will be blazing fast :)
keepDendenciesRunning()
}
}.with {
// Enables http client
// to make real http calls
// against the application under test
httpClient {
HttpClientSystemOptions(
baseUrl = "http://localhost:8001",
)
}
// Enables Couchbase physically
// and exposes the configuration
// to the application under test
couchbase {
CouchbaseSystemOptions(
defaultBucket = "Stove",
configureExposedConfiguration = { cfg -> listOf("couchbase.hosts=${cfg.hostsWithPort}") },
)
}
// Enables Kafka physically
// and exposes the configuration
// to the application under test
kafka {
KafkaSystemOptions(
configureExposedConfiguration = { cfg -> listOf("kafka.bootstrapServers=${cfg.boostrapServers}") },
)
}
// Enables Wiremock on the given port
// and provides configurable mock HTTP server
// for your external API calls
wiremock {
WireMockSystemOptions(
port = 9090,
removeStubAfterRequestMatched = true,
afterRequest = { e, _, _ ->
logger.info(e.request.toString())
},
)
}
// The Application Under Test.
// Enables Spring Boot application
// to be run with the given parameters.
springBoot(
runner = { parameters ->
stove.spring.example.run(parameters) { it.addTestSystemDependencies() }
},
withParameters = listOf(
"server.port=8001",
"logging.level.root=warn",
"logging.level.org.springframework.web=warn",
"spring.profiles.active=default",
"kafka.heartbeatInSeconds=2",
),
)
}.run()
Write Tests
TestSystem.validate {
wiremock {
mockGet("/example-url", responseBody = None, statusCode = 200)
}
http {
get<String>("/hello/index") { actual ->
actual shouldContain "Hi from Stove framework"
println(actual)
}
}
couchbase {
shouldQuery<Any>("SELECT * FROM system:keyspaces") { actual ->
println(actual)
}
}
kafka {
shouldBePublished<ExampleMessage> {
actual.aggregateId == 123
&& metadata.topic = "example-topic"
&& metadata.headers["example-header"] == "example-value"
}
shouldBeConsumed<ExampleMessage> {
actual.aggregateId == 123
&& metadata.topic = "example-topic"
&& metadata.headers["example-header"] == "example-value"
}
}
couchbase {
save(collection = "Backlogs", id = "id-of-backlog", instance = Backlog("id-of-backlog"))
}
http {
postAndExpectBodilessResponse("/backlog/reserve") { actual ->
actual.status.shouldBe(200)
}
}
kafka {
shouldBeConsumed<ProductCreated> {
actual.aggregateId == expectedId
}
}
}
Why Stove?
The JVM ecosystem lacks a unified approach to end-to-end testing. While tools like Testcontainers exist, developers still need to:
- Write extensive boilerplate code
- Complex setup code for each tech stack
- Create different testing setups for each framework
- Manage complex infrastructure configurations for each framework
This affects teams across many tech stacks:
- Kotlin with Spring Boot/Ktor
- Java with Spring Boot/Micronaut/Quarkus
- Scala with Spring Boot
Stove solves these challenges by providing:
- A unified testing API across all JVM stacks
- Built-in support for common infrastructure
- Clean, Kotlin-based test syntax
- Reusable test containers for fast local development
Stove unifies the testing experience across all JVM stacks, making it easier to write clean, focused tests.
Resources
- π Documentation
- πΉ Youtube Session about how to use (Turkish)
- π Motivation Article
Status
[!WARNING] While Stove is production-ready and extensively used, the API is not yet fully stabilized. Breaking changes may occur in minor releases, but migration guides will always be provided.
Contributing
Contributions are welcome! Whether it's:
- π Bug reports
- π‘ Feature requests
- π Documentation improvements
- π Code contributions
License
Stove is licensed under the Apache License, Version 2.0. See LICENSE for the full license text.