Skip to content

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.

Stove().with {
    bridge()
    springBoot(runner = { params -> com.app.run(params) })
}.run()

Ktor auto-detects Koin vs Ktor-DI vs custom (see Ktor guide).

The using<T> { } DSL

Inside stove { }, request beans from the AUT:

stove {
  using<OrderRepository> {
    val all = findAll()
    all shouldHaveSize 1
  }
}

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.

springBoot(
  runner = { params ->
    com.app.run(params) {
      addTestDependencies {
        bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)
        bean { StoveSerde.jackson.anyByteArraySerde() }
        bean<Clock>(isPrimary = true) { FixedClock(Instant.parse("2026-05-19T10:00:00Z")) }
      }
    }
  }
)
springBoot(
  runner = { params ->
    runApplication<MyApp>(*params) {
      addTestDependencies4x {
        registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)
        registerBean { StoveSerde.jackson.anyByteArraySerde() }
      }
    }
  }
)
ktor(runner = { params ->
  com.app.run(params, shouldWait = false, testModules = listOf(
    module {
      single<Clock>(override = true) { FixedClock(/* ... */) }
    }
  ))
})
ktor(runner = { params ->
  com.app.run(params, shouldWait = false) {
    provide<Clock> { FixedClock(/* ... */) }
  }
})
micronaut(runner = { params ->
  com.app.run(params) {
    registerSingleton(Clock::class.java, FixedClock(/* ... */), Qualifiers.byName("clock"))
  }
})

Real use cases

Test data setup

Seed via the app's own repository so all invariants apply.

State verification

Inspect cache contents, in-memory counters, processed-message lists.

Domain service drive

Call a service directly when there's no HTTP path (scheduled jobs, listeners).

Time control

Inject Clock / TimeProvider for deterministic tests.

Event capture

Register an @EventListener to assert on domain events.

Trigger background work

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

**Drive through HTTP / Kafka / DB if you can.** That's what production uses.
http { post<OrderResponse>("/orders", body) { /* assert */ } }
kafka {
  shouldBePublished<OrderCreated> {
    actual.id == id
  }
}
**Don't bypass the API to "set up" production state.**
using<OrderRepository> { save(prebuiltOrder) }
http { get("/orders/$id") { /* assert */ } }
You're testing the test setup, not the app.

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