Multiple Systems¶
By default, Stove registers one instance per system type --- one PostgreSQL, one Kafka, one HTTP client. With multiple systems, you can register multiple instances of the same system type, each identified by a typed key.
When to Use¶
- Microservice integration --- call multiple services, each with its own HTTP client or gRPC stub
- Multiple databases --- verify state in separate PostgreSQL or MongoDB instances
- Multi-cluster Kafka --- publish/consume from different Kafka clusters
- Cross-service verification --- after calling your app, check that dependent services received the right data
Define Keys¶
Keys are Kotlin singleton objects implementing SystemKey:
object OrderService : SystemKey
object PaymentService : SystemKey
object AppDb : SystemKey
object AnalyticsDb : SystemKey
Why Objects Instead of Strings?
Kotlin objects give you compile-time safety, IDE autocomplete, and refactor-safe references. Typos become compile errors. The same key can be reused across protocols --- httpClient(PaymentService) and grpc(PaymentService) both refer to the same logical service.
Configure¶
Pass the key as the first argument to any system DSL function:
Stove().with {
// Default HTTP client --- your app
httpClient {
HttpClientSystemOptions(baseUrl = "https://myapp.staging.com")
}
// Keyed HTTP clients --- dependent services
httpClient(OrderService) {
HttpClientSystemOptions(baseUrl = "https://order.internal.com")
}
httpClient(PaymentService) {
HttpClientSystemOptions(baseUrl = "https://payment.internal.com")
}
// Keyed databases
postgresql(AppDb) {
PostgresqlOptions.provided(
jdbcUrl = "jdbc:postgresql://staging-db:5432/myapp",
host = "staging-db", port = 5432,
configureExposedConfiguration = { emptyList() }
)
}
postgresql(AnalyticsDb) {
PostgresqlOptions.provided(
jdbcUrl = "jdbc:postgresql://analytics-db:5432/analytics",
host = "analytics-db", port = 5432,
configureExposedConfiguration = { emptyList() }
)
}
providedApplication()
}.run()
Write Tests¶
Pass the key to the validation DSL:
test("create order, verify across services and databases") {
stove {
// Call the app (default HTTP --- no key)
http {
postAndExpectJson<OrderResponse>("/orders", body = request.some()) { order ->
order.id shouldNotBe null
}
}
// Verify order service received the order
http(OrderService) {
getResponse("/api/orders/$orderId") { resp ->
resp.status shouldBe 200
}
}
// Verify payment was processed
http(PaymentService) {
getResponse("/api/payments?orderId=$orderId") { resp ->
resp.status shouldBe 200
}
}
// Verify in app's database
postgresql(AppDb) {
shouldQuery<Order>("SELECT * FROM orders WHERE id = ?", listOf(orderId)) { rows ->
rows shouldHaveSize 1
}
}
// Verify analytics event landed
postgresql(AnalyticsDb) {
shouldQuery<AnalyticsEvent>("SELECT * FROM events WHERE order_id = ?", listOf(orderId)) { events ->
events.first().type shouldBe "ORDER_CREATED"
}
}
}
}
Supported Systems¶
All multi-instance dependency systems support keyed registration:
| Category | Systems |
|---|---|
| Databases | PostgreSQL, MySQL, MSSQL, MongoDB, Cassandra, Couchbase, Redis, Elasticsearch |
| Protocol clients | HTTP Client, gRPC |
| Messaging | Kafka |
| Mocking | WireMock, gRPC Mock |
Single-instance systems (Bridge, Tracing, Dashboard) and framework starters (springBoot(), ktor()) do not support keyed registration --- there is only one application under test.
Spring Kafka
The Spring Kafka starter (stove-spring-kafka) does not support keyed instances because it is tied to a single Spring application context. Use the standalone stove-kafka module if you need multiple Kafka instances.
Default and Keyed Coexist¶
Default (unkeyed) and keyed instances of the same type are independent:
// Registration
httpClient { HttpClientSystemOptions(baseUrl = "https://myapp.com") } // default
httpClient(OrderService) { HttpClientSystemOptions(baseUrl = "https://order.internal.com") } // keyed
// Validation
http { /* hits myapp.com */ } // default
http(OrderService) { /* hits order.internal.com */ } // keyed
Reporting¶
Keyed systems produce distinguishable names in reports and traces:
HTTP > GET /orders # default
HTTP [OrderService] > GET /api/orders/123 # keyed
HTTP [PaymentService] > GET /api/payments # keyed
PostgreSQL [AppDb] > shouldQuery > SELECT # keyed
PostgreSQL [AnalyticsDb] > shouldQuery # keyed
Error Handling¶
If you pass a key that wasn't registered, you get a clear runtime error:
SystemNotRegisteredException: HttpSystem was not registered.
No HttpSystem registered with key 'OrderService'
Combining with Provided Application¶
Keyed systems and providedApplication() are designed to work together for full black-box testing:
Stove().with {
// Your app's API
httpClient { HttpClientSystemOptions(baseUrl = "https://staging.myapp.com") }
// Dependent services and infrastructure
httpClient(OrderService) { HttpClientSystemOptions(baseUrl = "https://order.internal.com") }
postgresql(AppDb) {
PostgresqlOptions.provided(
jdbcUrl = "jdbc:postgresql://staging-db:5432/myapp",
host = "staging-db", port = 5432,
configureExposedConfiguration = { emptyList() }
)
}
kafka {
KafkaSystemOptions.provided(
bootstrapServers = "staging-kafka:9092",
configureExposedConfiguration = { emptyList() }
)
}
// App already running
providedApplication {
ProvidedApplicationOptions(
readiness = ReadinessStrategy.HttpGet(url = "https://staging.myapp.com/health")
)
}
}.run()
See also: Provided Application for testing against deployed apps.