Skip to content

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:

ktor(
    runner = { params -> com.yourcompany.orders.run(params, wait = false) },
    withParameters = listOf("server.port=8080")
)
micronaut(
    runner = { params -> com.yourcompany.orders.run(params) },
    withParameters = listOf("micronaut.server.port=8080")
)
quarkus(
    runner = { params -> com.yourcompany.orders.main(params) },
    withParameters = listOf("quarkus.http.port=8080")
)

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:

goApp(
    binaryPath = System.getProperty("go.app.binary")
        ?: error("go.app.binary system property not set"),
    target = ProcessTarget.Server(
        port = 8080,
        portEnvVar = "PORT",
        readiness = ReadinessStrategy.HttpGet(url = "http://localhost:8080/health")
    )
)

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.