Recipe: Order Placement Flow¶
Test a complete order placement: client calls POST /orders, app validates inventory through a third-party HTTP service, persists the order, then publishes an OrderCreated event to Kafka. The test covers four external surfaces: HTTP in, HTTP out, DB state, and Kafka out.
Open this setup in the wizard ยท Source on GitHub โ
Systems used¶
| Surface | System | Why |
|---|---|---|
| HTTP in | HTTP Client | drive the app like a real client |
| HTTP out | WireMock | mock the inventory service |
| Persistence | PostgreSQL | verify the order row |
| Messaging | Kafka | assert the OrderCreated event |
Optional but recommended: Tracing and Dashboard. When enabled, they add a call chain and timeline to failures.
What success looks like¶
sequenceDiagram
autonumber
participant Test as Stove test
participant App as Your app
participant WM as WireMock<br/>(inventory)
participant PG as PostgreSQL
participant K as Kafka
Test->>App: POST /orders
App->>WM: GET /inventory/item-1
WM-->>App: 200 {inStock:true}
App->>PG: INSERT orders
App->>K: publish OrderCreated
App-->>Test: 201 {id, status}
Test->>PG: SELECT FROM orders โ
Test->>K: shouldBePublished<OrderCreated> โ
Gradle dependencies¶
plugins {
id("com.trendyol.stove.tracing") version "$stoveVersion"
}
stoveTracing {
serviceName.set("order-service")
testTaskNames.set(listOf("e2eTest"))
}
dependencies {
testImplementation(platform("com.trendyol:stove-bom:$stoveVersion"))
testImplementation("com.trendyol:stove")
testImplementation("com.trendyol:stove-spring")
testImplementation("com.trendyol:stove-extensions-kotest")
testImplementation("com.trendyol:stove-http")
testImplementation("com.trendyol:stove-postgres")
testImplementation("com.trendyol:stove-kafka")
testImplementation("com.trendyol:stove-wiremock")
testImplementation("com.trendyol:stove-tracing")
testImplementation("com.trendyol:stove-dashboard")
}
StoveConfig.kt¶
package com.yourcompany.orders.e2e.setup
import com.trendyol.stove.system.Stove
import com.trendyol.stove.http.HttpClientSystemOptions
import com.trendyol.stove.http.httpClient
import com.trendyol.stove.dashboard.*
import com.trendyol.stove.postgres.*
import com.trendyol.stove.kafka.*
import com.trendyol.stove.serialization.StoveSerde
import com.trendyol.stove.spring.*
import io.kotest.core.config.AbstractProjectConfig
import com.trendyol.stove.extensions.kotest.StoveKotestExtension
import com.trendyol.stove.tracing.tracing
import com.trendyol.stove.wiremock.*
import io.kotest.core.extensions.Extension
class StoveConfig : AbstractProjectConfig() {
override val extensions: List<Extension> = listOf(StoveKotestExtension())
override suspend fun beforeProject() {
Stove().with {
httpClient { HttpClientSystemOptions(baseUrl = "http://localhost:8080") }
tracing { enableSpanReceiver() }
dashboard { DashboardSystemOptions(appName = "order-service") }
wiremock {
WireMockSystemOptions(
port = 0,
configureExposedConfiguration = { cfg ->
listOf("clients.inventory.url=${cfg.baseUrl}")
}
)
}
postgresql {
PostgresqlOptions(
databaseName = "orders",
configureExposedConfiguration = { cfg ->
listOf(
"spring.datasource.url=${cfg.jdbcUrl}",
"spring.datasource.username=${cfg.username}",
"spring.datasource.password=${cfg.password}"
)
}
)
}
kafka {
KafkaSystemOptions(
serde = StoveSerde.jackson.anyByteArraySerde(),
configureExposedConfiguration = { cfg ->
listOf(
"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}",
"spring.kafka.producer.properties.interceptor.classes=${cfg.interceptorClass}",
"spring.kafka.consumer.properties.interceptor.classes=${cfg.interceptorClass}"
)
}
)
}
springBoot(
runner = { params ->
com.yourcompany.orders.run(params) {
addTestDependencies {
bean { StoveSerde.jackson.anyByteArraySerde() }
}
}
},
withParameters = listOf("server.port=8080")
)
}.run()
}
override suspend fun afterProject() = Stove.stop()
}
The test¶
class OrderFlowE2ETest : FunSpec({
test("POST /orders creates row and publishes OrderCreated") {
val userId = "user-${UUID.randomUUID()}"
val itemId = "item-1"
stove {
wiremock {
mockGet(
url = "/inventory/$itemId",
statusCode = 200,
responseBody = InventoryResponse(inStock = true).some()
)
}
http {
postAndExpectBody<OrderResponse>(
uri = "/orders",
body = CreateOrderRequest(userId, itemId, quantity = 1).some()
) { response ->
response.status shouldBe 201
response.body().orElseThrow().status shouldBe "CREATED"
}
}
postgresql {
shouldQuery<OrderRow>(
query = "SELECT id, user_id, status FROM orders WHERE user_id = '$userId'",
mapper = { row -> OrderRow(row.string("id"), row.string("user_id"), row.string("status")) }
) { rows ->
rows shouldHaveSize 1
rows.first().status shouldBe "CREATED"
}
}
kafka {
shouldBePublished<OrderCreated> {
actual.userId == userId && actual.itemId == itemId
}
}
}
}
})
Variations¶
Replace the runner block and keep the same system registrations:
Replace stove-spring with stove-process, configure Kafka via the Go bridge, compile the Go binary before the test task, pass its path as go.app.binary, and map system configuration into env vars or CLI args:
Common pitfalls¶
Kafka assertion times out
Default Kafka client settings are tuned for throughput, not test speed. Set linger.ms=0, batch.size=1, auto-offset-reset=earliest. See Kafka test-friendly settings.
WireMock stub never matches
Your app's external URL must point to WireMock. Use the configureExposedConfiguration lambda to inject clients.inventory.url=${cfg.baseUrl} and verify the app reads that property.
Database row not found
Confirm the table name matches your migrations. If you use Flyway, register migrations in PostgresqlOptions.migrations { register<InitialMigration>() }.
Diagnosing failures
With tracing and dashboard configured, a failure can show the call chain and timeline. See When a test fails.