An open API service indexing awesome lists of open source software.

https://github.com/trendyol/stove

Stove: The easiest way of writing e2e/component tests for your JVM back-end app with Kotlin
https://github.com/trendyol/stove

component-testing e2e-testing integration-testing java kotlin ktor spring-boot test test-automation testcontainers testing testing-framework testing-tools

Last synced: about 1 month ago
JSON representation

Stove: The easiest way of writing e2e/component tests for your JVM back-end app with Kotlin

Awesome Lists containing this project

README

          

Stove


End-to-end testing framework for the JVM.

Test your application against real infrastructure with a unified Kotlin DSL.


Release
Snapshot
codecov
OpenSSF Scorecard

```kotlin
stove {
// Call API and verify response
http {
postAndExpectBodilessResponse("/orders", body = CreateOrderRequest(userId, productId).some()) {
it.status shouldBe 201
}
}

// Verify database state
postgresql {
shouldQuery("SELECT * FROM orders WHERE user_id = '$userId'", mapper = { row ->
Order(row.string("status"))
}) {
it.first().status shouldBe "CONFIRMED"
}
}

// Verify event was published
kafka {
shouldBePublished {
actual.userId == userId
}
}

// Access application beans directly
using {
getStock(productId) shouldBe 9
}
}
```

## Why Stove?

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](https://trendyol.github.io/stove/writing-custom-systems/))

## Getting Started

**1. Add dependencies**

```kotlin
dependencies {
// Import BOM for version management
testImplementation(platform("com.trendyol:stove-bom:$version"))

// Core and framework starter
testImplementation("com.trendyol:stove")
testImplementation("com.trendyol:stove-spring") // or stove-ktor, stove-micronaut

// Component modules
testImplementation("com.trendyol:stove-postgres")
testImplementation("com.trendyol:stove-kafka")
}
```

> **Snapshots:** As of 5th June 2025, Stove's snapshot packages are hosted on [Central Sonatype](https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/com/trendyol/).
> ```kotlin
> repositories {
> maven("https://central.sonatype.com/repository/maven-snapshots")
> }
> ```

**2. Configure Stove** (runs once before all tests)

```kotlin
class TestConfig : AbstractProjectConfig() {
override suspend fun beforeProject() = Stove()
.with {
httpClient {
HttpClientSystemOptions(baseUrl = "http://localhost:8080")
}
postgresql {
PostgresqlOptions(
cleanup = { it.execute("TRUNCATE orders, users") },
configureExposedConfiguration = { listOf("spring.datasource.url=${it.jdbcUrl}") }
).migrations {
register()
}
}
kafka {
KafkaSystemOptions(
cleanup = { it.deleteTopics(listOf("orders")) },
configureExposedConfiguration = { listOf("kafka.bootstrapServers=${it.bootstrapServers}") }
).migrations {
register()
}
}
bridge()
springBoot(runner = { params ->
myApp.run(params) { addTestDependencies() }
})
}.run()

override suspend fun afterProject() = Stove.stop()
}
```

**3. Write tests**

```kotlin
test("should process order") {
stove {
http {
get("/orders/123") {
it.status shouldBe "CONFIRMED"
}
}
postgresql {
shouldQuery("SELECT * FROM orders", mapper = { row ->
Order(row.string("status"))
}) {
it.size shouldBe 1
}
}
kafka {
shouldBePublished {
actual.orderId == "123"
}
}
}
}
```

## Writing Tests

All assertions happen inside `stove { }`. Each component has its own DSL block.

### HTTP

```kotlin
http {
get("/users/$id") {
it.name shouldBe "John"
}
postAndExpectBodilessResponse("/users", body = request.some()) {
it.status shouldBe 201
}
postAndExpectBody("/users", body = request.some()) {
it.id shouldNotBe null
}
}
```

### Database

```kotlin
postgresql { // also: mongodb, couchbase, mssql, elasticsearch, redis
shouldExecute("INSERT INTO users (name) VALUES ('Jane')")
shouldQuery("SELECT * FROM users", mapper = { row ->
User(row.string("name"))
}) {
it.size shouldBe 1
}
}
```

### Kafka

```kotlin
kafka {
publish("orders.created", OrderCreatedEvent(orderId = "123"))
shouldBeConsumed {
actual.orderId == "123"
}
shouldBePublished {
actual.orderId == "123"
}
}
```

### External API Mocking

```kotlin
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()`:

```kotlin
using { processOrder(orderId) }
using { userRepo, emailService ->
userRepo.findById(id) shouldNotBe null
}
```

### Reporting

When tests fail, Stove automatically enriches exceptions with a detailed execution report showing exactly what happened:

Example Report

```
╔══════════════════════════════════════════════════════════════════════════════════════════════════╗
║ STOVE TEST EXECUTION REPORT ║
║ ║
║ Test: should create new product when send product create request from api for the allowed ║
║ supplier ║
║ ID: ExampleTest::should create new product when send product create request from api for the ║
║ allowed supplier ║
║ Status: FAILED ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ TIMELINE ║
║ ──────── ║
║ ║
║ 12:41:12.371 ✓ PASSED [WireMock] Register stub: GET /suppliers/99/allowed ║
║ Output: kotlin.Unit ║
║ Metadata: {statusCode=200, responseHeaders={}} ║
║ ║
║ 12:41:13.405 ✓ PASSED [HTTP] POST /api/product/create ║
║ Input: ProductCreateRequest(id=1, name=product name, supplierId=99) ║
║ Output: kotlin.Unit ║
║ Metadata: {status=200, headers={}} ║
║ ║
║ 12:41:13.424 ✓ PASSED [Kafka] shouldBePublished ║
║ Output: ProductCreatedEvent(id=1, name=product name, supplierId=99, createdDate=Thu Jan 08 ║
║ 12:41:12 CET 2026, type=ProductCreatedEvent) ║
║ Metadata: {timeout=5s} ║
║ ║
║ 12:41:13.455 ✗ FAILED [Couchbase] Get document ║
║ Input: {id=product:1} ║
║ Error: expected:<100L> but was:<99L> ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ SYSTEM SNAPSHOTS ║
║ ──────────────── ║
║ ║
║ ┌─ HTTP ──────────────────────────────────────────────────────────────────────────────────────── ║
║ ║
║ No detailed state available ║
║ ║
║ ┌─ COUCHBASE ─────────────────────────────────────────────────────────────────────────────────── ║
║ ║
║ No detailed state available ║
║ ║
║ ┌─ KAFKA ─────────────────────────────────────────────────────────────────────────────────────── ║
║ ║
║ Consumed: 0 ║
║ Published: 1 ║
║ Committed: 0 ║
║ ║
║ State Details: ║
║ consumed: 0 item(s) ║
║ published: 1 item(s) ║
║ [0] ║
║ id: 376db940-a367-4419-a628-4754c9466421 ║
║ topic: stove-standalone-example.productCreated.1 ║
║ key: 1 ║
║ headers: {X-EventType=ProductCreatedEvent, X-MessageId=29902970-056d-4ae9-9a84-...} ║
║ message: {"id":1,"name":"product name","supplierId":99,...} ║
║ committed: 0 item(s) ║
║ ║
║ ┌─ WIREMOCK ──────────────────────────────────────────────────────────────────────────────────── ║
║ ║
║ Registered stubs: 0 ║
║ Served requests: 0 (matched: 0) ║
║ Unmatched requests: 0 ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════╝
```

**Features:**
- Timeline of all operations with timestamps and results
- Input/output for each action
- Expected vs actual values on failures
- System snapshots (Kafka messages, WireMock stubs, etc.)

**Test Framework Extensions:**

Use the provided extensions to automatically enrich failures:

```kotlin
// Kotest - register in project config
class TestConfig : AbstractProjectConfig() {
override val extensions = listOf(StoveKotestExtension())
}

// JUnit 5 - annotate test class
@ExtendWith(StoveJUnitExtension::class)
class MyTest { ... }
```

**Configuration:**

```kotlin
Stove(
StoveOptions(
reportingEnabled = true, // Enable/disable reporting (default: true)
dumpReportOnTestFailure = true, // Enrich failures with report (default: true)
failureRenderer = PrettyConsoleRenderer // Custom renderer (default: PrettyConsoleRenderer)
)
).with { ... }
```

## Configuration

### Framework Setup

Spring BootKtorMicronaut

```kotlin
springBoot(
runner = { params ->
myApp.run(params) {
addTestDependencies()
}
}
)
```

```kotlin
ktor(
runner = { params ->
myApp.run(params) {
addTestDependencies()
}
}
)
```

```kotlin
micronaut(
runner = { params ->
myApp.run(params) {
addTestDependencies()
}
}
)
```

### Container Reuse

Speed up local development by keeping containers running between test runs:

```kotlin
Stove { keepDependenciesRunning() }.with { ... }
```

### Cleanup

Run cleanup logic after tests complete:

```kotlin
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:

```kotlin
postgresql {
PostgresqlOptions(...)
.migrations {
register()
register()
}
}
```

Available for Kafka, PostgreSQL, MongoDB, Couchbase, MSSQL, Elasticsearch, Redis.

### Provided Instances

Connect to existing infrastructure instead of starting containers (useful for CI/CD):

```kotlin
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

```kotlin
test("should create order with payment processing") {
stove {
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(
"/orders",
body = CreateOrderRequest(userId, productId).some()
) {
it.status shouldBe 201
}
}

// 4. Verify database
postgresql {
shouldQuery("SELECT * FROM orders WHERE user_id = '$userId'", mapper = { row ->
Order(row.string("status"))
}) {
it.first().status shouldBe "CONFIRMED"
}
}

// 5. Verify event published
kafka {
shouldBePublished {
actual.userId == userId
}
}

// 6. Verify via application service
using { 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 | Client (grpc-kotlin), Mock Server (native) |
| 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 |
| gRPC Mock | 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](https://trendyol.github.io/stove/Components/11-provided-instances/).

## Resources

- **[Documentation](https://trendyol.github.io/stove/)**: Full guides and API reference
- **[Examples](https://github.com/Trendyol/stove/tree/main/examples)**: Working sample projects
- **[Blog Post](https://medium.com/trendyol-tech/a-new-approach-to-the-api-end-to-end-testing-in-kotlin-f743fd1901f5)**:
Motivation and design decisions
- **[Video Walkthrough](https://youtu.be/DJ0CI5cBanc?t=669)**: Live demo (Turkish)

## Community

**Used by:**

1. [Trendyol](https://www.trendyol.com): Leading e-commerce platform, Turkey

*Using Stove? Open a PR to add your company.*

**Contributions:** [Issues](https://github.com/Trendyol/stove/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.