Troubleshooting¶
Use this page from the symptom back to the runtime boundary that failed: Docker, dependency startup, application boot, configuration injection, serializers, async assertions, or observability.
Read the failure report first With the Kotest or JUnit extension registered, failures include the Stove timeline (HTTP calls, database operations, Kafka observations, WireMock matches) above the assertion failure. Check that report first; it usually identifies which system saw the last successful operation.
Quick lookup¶
| Symptom | Section |
|---|---|
| Docker not found / not running | Docker |
| Port conflicts | Docker |
| Container slow / OOM | Containers + memory |
| App fails to start | App startup |
| Test bean isn't being applied | App startup |
Timed out waiting for condition |
Assertion timeouts |
| JSON parse / Mismatched input | Serialization mismatch |
| Kafka message never seen | Kafka assertions silent |
| WireMock stub never hits | WireMock not matching |
| Document / row not found | Data not found |
| CI parallel run collisions | Shared infrastructure |
| Dashboard / MCP empty | Dashboard + MCP |
| AI agent can't reach MCP | Dashboard + MCP |
| Old Stove version still in cache | Migrating versions |
Docker¶
Docker not found / not running¶
| Fix | When |
|---|---|
| Start Docker Desktop / colima / lima | Most common |
Run docker info from the same shell that runs Gradle |
Confirms the daemon is reachable from the test process |
| Use Provided Instances | Docker unavailable in CI |
Port conflicts¶
| Fix | Where |
|---|---|
Use dynamic port 0 for mocks |
WireMockSystemOptions(port = 0), GrpcMockSystemOptions(port = 0) |
| Stop the other process | lsof -i :8080 then kill |
| Pick a different fixed port | Last resort |
Containers + memory¶
OutOfMemoryError¶
JVM-side:
Container-side (example: Elasticsearch):
elasticsearch {
ElasticsearchSystemOptions(
container = ElasticContainerOptions(
containerFn = { c ->
c.withEnv("ES_JAVA_OPTS", "-Xms512m -Xmx512m")
}
)
) { /* config */ }
}
For CI, prefer Provided Instances to skip containers entirely.
Slow container startup¶
| Cause | Fix |
|---|---|
| Image pull on first run | Pre-pull in CI cache step |
| Dependency initialization time | Increase startup timeout per system, or use keepDependenciesRunning() locally |
| Health check slow | Provide an explicit readinessStrategy (HTTP / TCP / Probe) |
Keep containers warm during dev:
Disable in CI for clean runs.
App startup¶
App doesn't start¶
| Symptom | Fix |
|---|---|
Spring BeanCreationException |
Treat it as an application configuration error; inspect the root cause and fix the bean |
port already in use |
The app's port matches another process; pass a different server.port= via withParameters |
| Runner never reaches readiness | For Ktor, start with wait = false; for Quarkus, use the documented Quarkus.run(*args) startup pattern and readiness signal |
Test bean override doesn't apply¶
For Ktor, see the Ktor guide · Bridge auto-detection.
Assertion timeouts¶
Check the boundary where Stove expected to observe progress:
- The operation actually triggers. Add a
printlnorusing<X> { ... }inspection to confirm the code path fires. - Async work has time to complete. Default Stove timeouts are 5–10 seconds; increase per assertion if your async is slow.
- Interceptor / bridge is registered. Without the capture mechanism, Stove can drive the app but cannot observe the event. See Kafka assertions silent below.
- Topic / collection / table names match production. Off-by-one names = silent miss.
- App's offsets aren't behind. Add
auto-offset-reset=earliestfor fresh consumers.
Serialization mismatch¶
JsonParseException: Unrecognized field
MismatchedInputException: Cannot deserialize
Field is unexpectedly null
Stove's serde and your app's ObjectMapper must agree across HTTP, Kafka, and mocks. Align them:
val mapper = ObjectMapper().apply {
registerModule(KotlinModule.Builder().build())
registerModule(JavaTimeModule())
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
Stove().with {
http {
HttpClientSystemOptions(
baseUrl = "...",
contentConverter = JacksonConverter(mapper)
)
}
kafka {
KafkaSystemOptions(
serde = StoveSerde.jackson.anyByteArraySerde(mapper)
) { /* config */ }
}
wiremock {
WireMockSystemOptions(
serde = StoveSerde.jackson.anyByteArraySerde(mapper)
)
}
}
Other gotchas:
| Symptom | Fix |
|---|---|
| Kotlin data class field always null | Add KotlinModule + default values, or @JsonCreator |
| Field name renamed in JSON | Annotate with @JsonProperty("exactName") |
| Date/time fails to parse | JavaTimeModule + matching WRITE_DATES_AS_TIMESTAMPS flag |
Kafka assertions silent¶
Walk through the Kafka flow widget on the Kafka page; it shows visually where messages pass through the bridge.
Checklist:
-
Bridge interceptor registered in your AUT:
-
App reads
kafka.interceptorClassesfrom properties: -
Topic names match production exactly. Verify with:
-
Consumer starts from
earliestso it doesn't miss a message published before the listener attached: -
Test-friendly client settings. Default producer/consumer settings are tuned for production throughput. See Kafka · test-friendly settings.
For non-JVM apps (Go, Python, ...), make sure the stove-kafka bridge is wired into your producer/consumer.
WireMock not matching¶
Connection refused to external service
Test timeout when calling mocked endpoint
Mock not found / unexpected request
Most WireMock failures are configuration failures: the application under test is still calling the real URL, or a fixed test URL, instead of the WireMock base URL exposed by Stove.
Stove().with {
wiremock {
WireMockSystemOptions(
port = 0,
configureExposedConfiguration = { cfg ->
listOf(
"payment.service.url=${cfg.baseUrl}",
"inventory.service.url=${cfg.baseUrl}",
"notification.service.url=${cfg.baseUrl}"
)
}
)
}
springBoot(runner = { params -> com.app.run(params) })
}
And your app must read each URL from a property, not hardcode it:
Debug what WireMock actually received:
wiremock {
WireMock.getAllServeEvents().forEach { e ->
println("${e.request.method} ${e.request.url}")
}
}
Data not found¶
| Cause | Fix |
|---|---|
| Collection/table name mismatch | Mirror your app's naming exactly |
| Async write hasn't landed | Use a Stove polling assertion, not an immediate raw client read |
| Test cleanup ran early | Cleanup hooks run in afterProject, not between tests; if you need per-test isolation, use unique IDs |
| Wrong index (ES). Near-real-time delay | client().indices().refresh() before the assertion |
Shared infrastructure¶
Tests pass locally but fail randomly in CI
Data from another test run appears
"Topic already exists" / "Index already exists"
Cause: multiple test runs share resource names. Fix with unique resource prefixes per run and pass those names into the application configuration.
object TestRunContext {
val runId: String = System.getenv("CI_JOB_ID")
?: UUID.randomUUID().toString().take(8)
val databaseName = "testdb_$runId"
val topicPrefix = "test_${runId}_"
val indexPrefix = "test_${runId}_"
}
Apply everywhere your app addresses these resources:
springBoot(withParameters = listOf(
"spring.datasource.url=jdbc:postgresql://db:5432/${TestRunContext.databaseName}",
"kafka.topic.orders=${TestRunContext.topicPrefix}orders",
"elasticsearch.index.products=${TestRunContext.indexPrefix}products"
))
Clean up only what you created:
cleanup = { admin ->
admin.listTopics().names().get()
.filter { it.startsWith(TestRunContext.topicPrefix) }
.takeIf { it.isNotEmpty() }
?.let { admin.deleteTopics(it).all().get() }
}
Always log the run ID at suite start so CI failures can be mapped back to the exact resources created by that run.
Deep dive: Provided Instances · isolation.
Dashboard + MCP¶
| Symptom | Fix |
|---|---|
Dashboard at http://localhost:4040 empty |
stove CLI running? dashboard { } registered in Stove().with? appName set? |
gRPC disabled warning in logs |
CLI started after tests; start the CLI before the test suite |
| Agent can't connect to MCP | Endpoint is on the CLI, not the test JVM. Verify http://localhost:4040/api/v1/meta returns "mcp": { "enabled": true } |
stove_trace returns nothing |
Tracing not enabled |
| Disk filling with old run data | Run stove --clear to wipe stored runs from ~/.stove-dashboard.db |
MCP is optional. It gives agents a structured way to read the same failure evidence humans see in the console and dashboard.
Migrating versions¶
| Bump | Notable change | Notes |
|---|---|---|
0.14 → 0.15 |
StoveSerde replaces direct ObjectMapper |
Release notes |
0.21 → 0.21.2 |
configureStoveTracing → stoveTracing (buildSrc); new Gradle plugin available |
Release notes |
0.21.2 → 0.22 |
Mordant console reporting; stove-quarkus module |
No breaking changes |
0.22 → 0.23 |
Dashboard launched | Opt-in, non-blocking |
0.23 → 0.24 |
Polyglot leap (provided app, keyed systems, process/container, Go, MCP) | All additive |
Pin the BOM, all com.trendyol:stove-* dependencies, the tracing Gradle plugin, and stove-cli to one Stove version.
Mixed versions are a common cause of class-load errors and empty dashboard data.
Common FAQ¶
Can I use Stove with Java?
Yes for the AUT. Stove's test DSL itself is Kotlin-only. Java apps get tested by Kotlin test files.
Can I use JUnit instead of Kotest?
Yes. See Getting Started for both setups.
How do I debug tests?
Set breakpoints in app code, run tests in debug mode, enable verbose logging via logging.level.root=debug, and use using<T> { ... } to inspect bean state.
Can I run tests in parallel?
Yes, with unique test data (UUIDs) and no shared state. For shared infra, use the isolation pattern.
How do I test with TLS/SSL?
Configure the system with security on (e.g. ElasticContainerOptions(disableSecurity = false)) and read cfg.certificate in configureExposedConfiguration.
Can I use custom container images / registries?
Yes. Per-system container = ...ContainerOptions(registry = "...", image = "...", tag = "...") or globally via DEFAULT_REGISTRY = "...".
Can I access the underlying Testcontainer?
pause() / unpause() from inside stove { }; client() for the underlying client (Lettuce, ES client, etc.); containerFn = { c -> ... } for one-off GenericContainer tweaks.
How do I handle DB migrations?
postgresql { PostgresqlOptions(...).migrations { register<MyMigration>() } }. Same shape across all SQL/NoSQL systems.
How do I test multiple databases / Kafka clusters?
Use keyed systems. postgresql(AppDb) { ... } plus postgresql(AnalyticsDb) { ... }.
Can I share test setup across modules?
Yes. Extract StoveConfig and the test base class into a shared test-extensions module.
Still stuck?¶
- GitHub Issues. Search first, then file
- Examples and Recipes
- New issue. Include Stove version, JDK version, Docker version, full error, minimal repro
Debug checklist¶
- Docker running and reachable (or Provided Instances wired)
- One pinned Stove version across BOM + every dep
- App's reusable
run(args)entrypoint extracted and exposed - Test config injected via
withParameters/configureExposedConfiguration - Serializers aligned (Stove serde === app
ObjectMapper) - Kafka interceptor registered as bean +
kafka.interceptorClassesproperty mapped - External URLs in app are properties, not hardcoded
- Per-run prefixes on shared infra
- Containers have enough memory
- Ports free or
port = 0for mocks - Test extension registered (
StoveKotestExtension()or@ExtendWith(StoveJUnitExtension)) - For tracing:
stoveTracingGradle plugin applied +tracing { enableSpanReceiver() }in setup