Bridge¶
Reach into your app's DI container from a test. Read internal state, drive domain services, replace beans for the test only. Type-safe, framework-aware.
When to use Bridge
You need state or behavior that isn't exposed through HTTP/Kafka/DB surfaces. Verifying internal flags, exercising a domain method directly, swapping Clock for deterministic time, capturing emitted domain events. If a public surface tells you the answer, prefer that instead.
Quarkus
Bridge isn't shipped for stove-quarkus (CDI lifecycle). Drive verification through HTTP, DB, Kafka, gRPC.
Setup¶
Built into the framework starters. No extra dependency.
Ktor auto-detects Koin vs Ktor-DI vs custom (see Ktor guide).
The using<T> { } DSL¶
Inside stove { }, request beans from the AUT:
Multi-bean fetch. Up to 5 generic args:
stove {
using<OrderService, PaymentService> { orderSvc, paySvc ->
orderSvc.place(order)
paySvc.charge(order.amount)
}
}
The lambda receives beans in declaration order. Generics resolve correctly (using<List<Validator>> { ... } returns the bound list).
Capture values out of the lambda¶
stove {
val savedOrder = using<OrderRepository> {
findById(orderId).orElseThrow()
}
savedOrder.status shouldBe "CREATED"
}
using<T> returns whatever the lambda returns.
Register test-only beans¶
Inject test doubles or interceptors at runner setup time. Pattern depends on framework + version.
Real use cases¶
Seed via the app's own repository so all invariants apply.
Inspect cache contents, in-memory counters, processed-message lists.
Call a service directly when there's no HTTP path (scheduled jobs, listeners).
Inject Clock / TimeProvider for deterministic tests.
Register an @EventListener to assert on domain events.
Manually advance a scheduled job, drain a retry queue, etc.
Time-controlled example¶
class FixedClock(private val instant: Instant) : Clock() {
override fun getZone() = ZoneOffset.UTC
override fun withZone(zone: ZoneId) = this
override fun instant() = instant
}
// Test
test("order expires after 24h") {
stove {
val clock = using<Clock> { this }
http { post("/orders", body) { it.status shouldBe 201 } }
(clock as FixedClock).advance(Duration.ofHours(25))
using<OrderService> {
getOrder(orderId).status shouldBe "EXPIRED"
}
}
}
Domain event capture¶
@Component
class TestEventCaptor {
val captured = ConcurrentLinkedQueue<DomainEvent>()
@EventListener fun on(e: DomainEvent) { captured.add(e) }
}
// Register in test dependencies, then assert:
stove {
http { post("/orders", body) { it.status shouldBe 201 } }
using<TestEventCaptor> {
eventually(10.seconds) {
captured.any { it is OrderCreated && it.id == orderId }
}
}
}
When NOT to use Bridge¶
Troubleshooting¶
| Symptom | Check |
|---|---|
bridge() not available |
Quarkus isn't supported yet; use HTTP/DB/Kafka assertions |
NoSuchBeanDefinitionException |
Bean exists in production code? Type matches the bound interface? |
| Ktor: wrong DI container detected | Pass explicit resolver: bridge { app, type -> myContainer.resolve(type) } |
| Test bean not overriding production | Spring 2.x/3.x: isPrimary = true. Spring 4.x: primary = true |
| Generics not resolving | Use using<List<T>> { } form; Stove resolves via Kotlin reified types |