stove
Stove
End-to-end testing framework for the JVM.
Test your application against real infrastructure with a unified Kotlin DSL.
validate {
// Call API and verify response
http {
postAndExpectBodilessResponse("/orders", body = CreateOrderRequest(userId, productId).some()) {
it.status shouldBe 201
}
}
// Verify database state
postgresql {
shouldQuery<Order>("SELECT * FROM orders WHERE user_id = '$userId'", mapper = { row ->
Order(row.string("status"))
}) {
it.first().status shouldBe "CONFIRMED"
}
}
// Verify event was published
kafka {
shouldBePublished<OrderCreatedEvent> {
actual.userId == userId
}
}
// Access application beans directly
using<InventoryService> {
getStock(productId) shouldBe 9
}
}
The JVM ecosystem has excellent frameworks for building applications, but e2e testing remains fragmented. Testcontainers handles infrastructure, but you still write boilerplate for configuration, app startup, and assertions. Differently for each framework.
Stove explores how the testing experience on the JVM can be improved by unifying assertions and the supporting infrastructure. It creates a concise and expressive testing DSL by leveraging Kotlin's unique language features.
Stove works with Java, Kotlin, and Scala applications across Spring Boot, Ktor, and Micronaut. Because tests are framework-agnostic, teams can migrate between stacks without rewriting test code. It empowers developers to write clear assertions even for code that is traditionally hard to test (async flows, message consumers, database side effects).
What Stove does:
- Starts containers via Testcontainers or connect provided infra (PostgreSQL, Kafka, etc.)
- Launches your actual application with test configuration
- Exposes a unified DSL for assertions across all components
- Provides access to your DI container from tests
- Debug your entire use case with one click (breakpoints work everywhere)
- Get code coverage from e2e test execution
- Supports Spring Boot, Ktor, Micronaut
- Extensible architecture for adding new components and frameworks (Writing Custom Systems)
Getting Started
1. Add dependencies
dependencies {
testImplementation("com.trendyol:stove-testing-e2e:$version")
testImplementation("com.trendyol:stove-spring-testing-e2e:$version") // or ktor, micronaut
testImplementation("com.trendyol:stove-testing-e2e-rdbms-postgres:$version")
testImplementation("com.trendyol:stove-testing-e2e-kafka:$version")
}
Snapshots: As of 5th June 2025, Stove's snapshot packages are hosted on Central Sonatype.
repositories { maven("https://central.sonatype.com/repository/maven-snapshots") }
2. Configure test system (runs once before all tests)
class TestConfig : AbstractProjectConfig() {
override suspend fun beforeProject() = TestSystem()
.with {
httpClient {
HttpClientSystemOptions(baseUrl = "http://localhost:8080")
}
postgresql {
PostgresqlOptions(
cleanup = { it.execute("TRUNCATE orders, users") },
configureExposedConfiguration = { listOf("spring.datasource.url=${it.jdbcUrl}") }
).migrations {
register<CreateUsersTable>()
}
}
kafka {
KafkaSystemOptions(
cleanup = { it.deleteTopics(listOf("orders")) },
configureExposedConfiguration = { listOf("kafka.bootstrapServers=${it.bootstrapServers}") }
).migrations {
register<CreateOrdersTopic>()
}
}
bridge()
springBoot(runner = { params ->
myApp.run(params) { addTestSystemDependencies() }
})
}.run()
override suspend fun afterProject() = TestSystem.stop()
}
3. Write tests
test("should process order") {
validate {
http {
get<Order>("/orders/123") {
it.status shouldBe "CONFIRMED"
}
}
postgresql {
shouldQuery<Order>("SELECT * FROM orders", mapper = { row ->
Order(row.string("status"))
}) {
it.size shouldBe 1
}
}
kafka {
shouldBePublished<OrderCreatedEvent> {
actual.orderId == "123"
}
}
}
}
Writing Tests
All assertions happen inside validate { }. Each component has its own DSL block.
HTTP
http {
get<User>("/users/$id") {
it.name shouldBe "John"
}
postAndExpectBodilessResponse("/users", body = request.some()) {
it.status shouldBe 201
}
postAndExpectBody<User>("/users", body = request.some()) {
it.id shouldNotBe null
}
}
Database
postgresql { // also: mongodb, couchbase, mssql, elasticsearch, redis
shouldExecute("INSERT INTO users (name) VALUES ('Jane')")
shouldQuery<User>("SELECT * FROM users", mapper = { row ->
User(row.string("name"))
}) {
it.size shouldBe 1
}
}
Kafka
kafka {
publish("orders.created", OrderCreatedEvent(orderId = "123"))
shouldBeConsumed<OrderCreatedEvent> {
actual.orderId == "123"
}
shouldBePublished<OrderConfirmedEvent> {
actual.orderId == "123"
}
}
External API Mocking
wiremock {
mockGet("/external-api/users/1", responseBody = User(id = 1, name = "John").some())
mockPost("/external-api/notify", statusCode = 202)
}
Application Beans
Access your DI container directly via bridge():
using<OrderService> { processOrder(orderId) }
using<UserRepo, EmailService> { userRepo, emailService ->
userRepo.findById(id) shouldNotBe null
}
Configuration
Framework Setup
| Spring Boot | Ktor | Micronaut |
|---|---|---|
kotlin
springBoot(
runner = { params ->
myApp.run(params) {
addTestSystemDependencies()
}
}
)
|
kotlin
ktor(
runner = { params ->
myApp.run(params) {
addTestSystemDependencies()
}
}
)
|
kotlin
micronaut(
runner = { params ->
myApp.run(params) {
addTestSystemDependencies()
}
}
)
|
Container Reuse
Speed up local development by keeping containers running between test runs:
TestSystem { keepDependenciesRunning() }.with { ... }
Cleanup
Run cleanup logic after tests complete:
postgresql {
PostgresqlOptions(cleanup = { it.execute("TRUNCATE users") }, ...)
}
kafka {
KafkaSystemOptions(cleanup = { it.deleteTopics(listOf("test-topic")) }, ...)
}
Available for Kafka, PostgreSQL, MongoDB, Couchbase, MSSQL, Elasticsearch, Redis.
Migrations
Run database migrations before tests start:
postgresql {
PostgresqlOptions(...)
.migrations {
register<CreateUsersTable>()
register<CreateOrdersTable>()
}
}
Available for Kafka, PostgreSQL, MongoDB, Couchbase, MSSQL, Elasticsearch, Redis.
Provided Instances
Connect to existing infrastructure instead of starting containers (useful for CI/CD):
postgresql { PostgresqlOptions.provided(jdbcUrl = "jdbc:postgresql://ci-db:5432/test", ...) }
kafka { KafkaSystemOptions.provided(bootstrapServers = "ci-kafka:9092", ...) }
Tip: When using provided instances, use migrations to create isolated test schemas and cleanups to remove test data afterwards. This ensures test isolation on shared infrastructure.
Complete Example
test("should create order with payment processing") {
validate {
val userId = UUID.randomUUID().toString()
val productId = UUID.randomUUID().toString()
// 1. Seed database
postgresql {
shouldExecute("INSERT INTO users (id, name) VALUES ('$userId', 'John')")
shouldExecute("INSERT INTO products (id, price, stock) VALUES ('$productId', 99.99, 10)")
}
// 2. Mock external payment API
wiremock {
mockPost(
"/payments/charge", statusCode = 200,
responseBody = PaymentResult(success = true).some()
)
}
// 3. Call API
http {
postAndExpectBody<OrderResponse>(
"/orders",
body = CreateOrderRequest(userId, productId).some()
) {
it.status shouldBe 201
}
}
// 4. Verify database
postgresql {
shouldQuery<Order>("SELECT * FROM orders WHERE user_id = '$userId'", mapper = { row ->
Order(row.string("status"))
}) {
it.first().status shouldBe "CONFIRMED"
}
}
// 5. Verify event published
kafka {
shouldBePublished<OrderCreatedEvent> {
actual.userId == userId
}
}
// 6. Verify via application service
using<InventoryService> { getStock(productId) shouldBe 9 }
}
}
Reference
Supported Components
| Category | Components |
|---|---|
| Databases | PostgreSQL, MongoDB, Couchbase, MSSQL, Elasticsearch, Redis |
| Messaging | Kafka |
| HTTP | Built-in client, WebSockets, WireMock |
| gRPC | Wire, grpc-kotlin |
| Frameworks | Spring Boot, Ktor, Micronaut, Quarkus (experimental) |
Feature Matrix
| Component | Migrations | Cleanup | Provided Instance | Pause/Unpause |
|---|---|---|---|---|
| PostgreSQL | ✅ | ✅ | ✅ | ✅ |
| MSSQL | ✅ | ✅ | ✅ | ✅ |
| MongoDB | ✅ | ✅ | ✅ | ✅ |
| Couchbase | ✅ | ✅ | ✅ | ✅ |
| Elasticsearch | ✅ | ✅ | ✅ | ✅ |
| Redis | ✅ | ✅ | ✅ | ✅ |
| Kafka | ✅ | ✅ | ✅ | ✅ |
| WireMock | n/a | n/a | n/a | n/a |
| HTTP Client | n/a | n/a | n/a | n/a |
FAQ
Can I use Stove with Java applications?Yes. Your application can be Java, Scala, or any JVM language. Tests are written in Kotlin for the DSL. Does Stove replace Testcontainers?
No. Stove uses Testcontainers underneath and adds the unified DSL on top. How slow is the first run?
First run pulls Docker images (~1-2 min). Use
keepDependenciesRunning() for instant subsequent runs.
Can I run tests in parallel?Yes, with unique test data per test. See provided instances docs.
Resources
- Documentation: Full guides and API reference
- Examples: Working sample projects
- Blog Post: Motivation and design decisions
- Video Walkthrough: Live demo (Turkish)
Community
Used by:
- Trendyol: Leading e-commerce platform, Turkey
Using Stove? Open a PR to add your company.
Contributions: Issues and PRs welcome
License: Apache 2.0
Note: Production-ready and used at scale. API still evolving; breaking changes possible in minor releases with migration guides.
