Skip to content

WireMock

Mock third-party HTTP services at the network edge. Stubs for every verb, partial-body matching with dot notation, behavior sequences for retry / circuit-breaker tests.

Open in setup wizard

WireMock (HTTP mock) — wizard-synced snippet

Gradle

testImplementation("com.trendyol:stove-wiremock")

Stove configuration

Stove().with {
    wiremock {
      WireMockSystemOptions(
        port = 0,
        configureExposedConfiguration = { cfg ->
          listOf("clients.payment.url=${cfg.baseUrl}")
        }
      )
    }
}

Test DSL

stove {
    wiremock {
      mockGet("/inventory/item-1", statusCode = 200, responseBody = mapOf("inStock" to true).some())
    }
}

In 30 seconds Register wiremock { WireMockSystemOptions(port = 0) } (port 0 = dynamic, CI-safe). Inject the actual URL into your AUT via configureExposedConfiguration. In tests, call mockGet, mockPost, mockPut, mockPatch, mockDelete, mockHead. For complex matching, use mockPostContaining with dot notation.

Configure

Stove().with {
  wiremock {
    WireMockSystemOptions(
      // port = 0 by default (dynamic, CI-safe)
      configureExposedConfiguration = { cfg ->
        // cfg.baseUrl = "http://localhost:<dynamic-port>"
        listOf(
          "payment.service.url=${cfg.baseUrl}",
          "inventory.service.url=${cfg.baseUrl}",
          "notification.service.url=${cfg.baseUrl}"
        )
      }
    )
  }
}.run()

External URLs must be configurable in your app

WireMock can't intercept hardcoded URLs. Your app must read these as properties, not bake them into client classes. See Best Practices · External URLs.

Options

Field Default Use
port 0 (dynamic) Fixed port if needed; prefer 0
configure notifier(ConsoleNotifier(true)) Custom WireMockConfiguration builder
removeStubAfterRequestMatched false One-shot stubs
afterStubRemoved no-op Hook after stub eviction
afterRequest no-op Hook after each request
serde StoveSerde.jackson.anyByteArraySerde() Pass your app's mapper
configureExposedConfiguration empty Inject WireMock URL into AUT

Basic stubs

stove {
  wiremock {
    // GET
    mockGet(
      url = "/api/products/1",
      statusCode = 200,
      responseBody = Product("1", "Laptop", 999.99).some(),
      responseHeaders = mapOf("X-Rate-Limit" to "100")
    )

    // POST
    mockPost(
      url = "/api/orders",
      statusCode = 201,
      requestBody = CreateOrderRequest(items = listOf("item1")).some(),
      responseBody = OrderResponse(orderId = "o-123").some()
    )

    // PUT
    mockPut(
      url = "/api/products/1",
      statusCode = 200,
      requestBody = UpdateProductRequest(price = 899.99).some(),
      responseBody = Product("1", "Updated", 899.99).some()
    )

    // PATCH
    mockPatch(
      url = "/api/users/123",
      statusCode = 200,
      requestBody = mapOf("email" to "new@example.com").some(),
      responseBody = UserResponse(id = "123", email = "new@example.com").some()
    )

    // DELETE / HEAD (typically bodiless)
    mockDelete(url = "/api/products/123", statusCode = 204)
    mockHead(url = "/api/products/exists/123", statusCode = 200)
  }
}

Advanced matching

For URL patterns, regex bodies, header constraints:

stove {
  wiremock {
    mockGetConfigure(
      url = "/api/search",
      urlPatternFn = { urlPathMatching("/api/search.*") }
    ) { builder, serde ->
      builder
        .withQueryParam("q", matching(".*laptop.*"))
        .willReturn(
          aResponse()
            .withStatus(200)
            .withBody(serde.serialize(SearchResults(items = listOf("a", "b"))))
        )
    }

    mockPostConfigure(
      url = "/api/webhooks",
      urlPatternFn = { urlEqualTo(it) }
    ) { builder, _ ->
      builder
        .withHeader("X-Webhook-Secret", equalTo("secret123"))
        .withRequestBody(containing("event_type"))
        .willReturn(aResponse().withStatus(200).withBody("ok"))
    }
  }
}

Partial body matching (dot notation)

The killer feature. Match only the fields you care about; ignore generated IDs, timestamps, surrounding context.

stove {
  wiremock {
    // Only matches if productId == 123 (other fields ignored)
    mockPostContaining(
      url = "/api/orders",
      requestContaining = mapOf("productId" to 123),
      statusCode = 201,
      responseBody = OrderResponse(orderId = "o-1").some()
    )

    // Multiple fields: AND logic
    mockPostContaining(
      url = "/api/payments",
      requestContaining = mapOf(
        "orderId" to "o-1",
        "amount" to 99.99,
        "currency" to "USD"
      ),
      statusCode = 200,
      responseBody = PaymentResponse(transactionId = "t-1").some()
    )

    // Deep nesting via dot path
    mockPostContaining(
      url = "/api/orders",
      requestContaining = mapOf("order.customer.id" to "cust-123"),
      statusCode = 200,
      responseBody = OrderConfirmation(status = "confirmed").some()
    )

    // Mix levels
    mockPostContaining(
      url = "/api/checkout",
      requestContaining = mapOf(
        "order.customer.id" to "cust-123",
        "order.payment.method" to "credit_card",
        "metadata.source" to "mobile_app"
      ),
      statusCode = 200,
      responseBody = CheckoutResponse(success = true).some()
    )

    // Nested object: partial comparison (extra fields in nested object ignored)
    mockPutContaining(
      url = "/api/config",
      requestContaining = mapOf("settings" to mapOf("enabled" to true)),
      statusCode = 200
    )

    // URL pattern + partial body
    mockPostContaining(
      url = "/api/v[0-9]+/orders",
      requestContaining = mapOf("orderId" to "o-1"),
      statusCode = 200,
      urlPatternFn = { urlPathMatching(it) }
    )
  }
}
Method Verb
mockPostContaining POST
mockPutContaining PUT
mockPatchContaining PATCH

Supports primitives, nested maps, arrays, dot paths, and urlPatternFn for regex URLs.

Behavior sequences

Test retry, circuit-breaker, and recovery flows:

test("service recovers after two failures") {
  stove {
    wiremock {
      behaviourFor("/api/external-service", WireMock::get) {
        initially { aResponse().withStatus(503) }
        then      { aResponse().withStatus(503) }
        then      {
          aResponse()
            .withStatus(200)
            .withHeader("Content-Type", "application/json")
            .withBody(it.serialize(ServiceResponse(status = "OK")))
        }
      }
    }

    http {
      getResponse("/api/external-service") { it.status shouldBe 503 }
      getResponse("/api/external-service") { it.status shouldBe 503 }
      get<ServiceResponse>("/api/external-service") { it.status shouldBe "OK" }
    }
  }
}

Simulating slow responses

wiremock {
  mockGetConfigure("/slow-endpoint") { builder, _ ->
    builder.willReturn(
      aResponse()
        .withStatus(200)
        .withBody("Response")
        .withFixedDelay(5000)   // 5-second delay
    )
  }
}

Full example: order with three upstream services

test("order creation orchestrates user, product, inventory") {
  stove {
    val userId = "user-123"
    val productId = "product-456"

    wiremock {
      mockGet(
        url = "/users/$userId",
        statusCode = 200,
        responseBody = User(id = userId, name = "John", active = true).some()
      )

      mockGet(
        url = "/products/$productId",
        statusCode = 200,
        responseBody = Product(id = productId, name = "Laptop", price = 999.99).some()
      )

      mockPost(
        url = "/inventory/reserve",
        statusCode = 200,
        requestBody = ReserveStockRequest(productId = productId, quantity = 1).some(),
        responseBody = ReservationResponse(reservationId = "r-1", success = true).some()
      )
    }

    http {
      postAndExpectBody<OrderResponse>(
        uri = "/orders",
        body = CreateOrderRequest(userId = userId, productId = productId, quantity = 1).some()
      ) {
        it.status shouldBe 201
        it.body().status shouldBe "CREATED"
      }
    }

    kafka {
      shouldBePublished<OrderCreatedEvent> {
        actual.userId == userId && actual.productId == productId
      }
    }
  }
}

Pairs well with