Wiremock¶
Configure¶
After getting the library from the maven source, while configuring TestSystem you will have access to wiremock
function.
This will start an instance of Wiremock server. You can configure the port of the Wiremock server.
Options¶
data class WireMockSystemOptions(
/**
* Port of wiremock server
*/
val port: Int = 9090,
/**
* Configures wiremock server
*/
val configure: WireMockConfiguration.() -> WireMockConfiguration = { this.notifier(ConsoleNotifier(true)) },
/**
* Removes the stub when request matches/completes
* Default value is false
*/
val removeStubAfterRequestMatched: Boolean = false,
/**
* Called after stub removed
*/
val afterStubRemoved: AfterStubRemoved = { _, _ -> },
/**
* Called after request handled
*/
val afterRequest: AfterRequestHandler = { _, _ -> },
/**
* ObjectMapper for serialization/deserialization
*/
val serde: StoveSerde<Any, ByteArray> = StoveSerde.jackson.anyByteArraySerde()
) : SystemOptions
Mocking¶
Wiremock starts a mock server on the localhost with the given port. The important thing is that you use the same port
in your application for your services.
Say, your application calls an external service in your production configuration as:
http://externalservice.com/api/product/get-all
you need to replace the base url of this an all the external services with the Wiremock host and port:
http://localhost:9090
You can either do this in your application configuration, or let Stove send this as a command line argument to your application.
TestSystem()
.with {
wiremock {
WireMockSystemOptions(
port = 9090,
)
}
springBoot( // or ktor
runner = {
// ...
},
withParameters = listOf(
"externalServiceBaseUrl=http://localhost:9090",
"otherService1BaseUrl=http://localhost:9090",
"otherService2BaseUrl=http://localhost:9090"
)
)
}
.run()
All service endpoints will be pointing to the Wiremock server. You can now define the stubs for the services that your application calls.
Usage¶
GET Requests¶
Mock GET requests with various configurations:
TestSystem.validate {
wiremock {
// Simple GET mock
mockGet(
url = "/api/products",
statusCode = 200,
responseBody = listOf(
Product("1", "Laptop", 999.99),
Product("2", "Mouse", 29.99)
).some()
)
// GET with custom headers
mockGet(
url = "/api/user/profile",
statusCode = 200,
responseBody = UserProfile(id = "123", name = "John").some(),
responseHeaders = mapOf(
"Content-Type" to "application/json",
"X-Rate-Limit" to "100"
)
)
// GET returning error
mockGet(
url = "/api/products/999",
statusCode = 404,
responseBody = ErrorResponse("Product not found").some()
)
}
}
POST Requests¶
Mock POST requests with request/response bodies:
TestSystem.validate {
wiremock {
// POST with request and response body
mockPost(
url = "/api/orders",
statusCode = 201,
requestBody = CreateOrderRequest(items = listOf("item1", "item2")).some(),
responseBody = OrderResponse(orderId = "order-123", status = "CREATED").some()
)
// POST with metadata matching
mockPost(
url = "/api/users",
statusCode = 201,
requestBody = CreateUserRequest(name = "John").some(),
responseBody = UserResponse(id = "user-123", name = "John").some(),
metadata = mapOf("Content-Type" to "application/json")
)
// POST returning error
mockPost(
url = "/api/orders",
statusCode = 400,
requestBody = InvalidOrderRequest().some(),
responseBody = ValidationError("Invalid order data").some()
)
}
}
PUT Requests¶
Mock PUT requests for updates:
TestSystem.validate {
wiremock {
// PUT with full update
mockPut(
url = "/api/products/123",
statusCode = 200,
requestBody = UpdateProductRequest(name = "Updated Product", price = 899.99).some(),
responseBody = Product("123", "Updated Product", 899.99).some()
)
// PUT with no response body
mockPut(
url = "/api/settings/update",
statusCode = 204,
requestBody = UpdateSettingsRequest(theme = "dark").some()
)
}
}
PATCH Requests¶
Mock PATCH requests for partial updates:
TestSystem.validate {
wiremock {
// PATCH for partial update
mockPatch(
url = "/api/users/123",
statusCode = 200,
requestBody = mapOf("email" to "newemail@example.com").some(),
responseBody = UserResponse(id = "123", email = "newemail@example.com").some()
)
}
}
DELETE Requests¶
Mock DELETE requests:
TestSystem.validate {
wiremock {
// DELETE returning success
mockDelete(
url = "/api/products/123",
statusCode = 204
)
// DELETE with metadata
mockDelete(
url = "/api/users/456",
statusCode = 200,
metadata = mapOf("Authorization" to "Bearer token123")
)
}
}
HEAD Requests¶
Mock HEAD requests:
TestSystem.validate {
wiremock {
mockHead(
url = "/api/products/exists/123",
statusCode = 200
)
mockHead(
url = "/api/products/exists/999",
statusCode = 404
)
}
}
Advanced Configuration¶
For complex scenarios, use the configure methods:
TestSystem.validate {
wiremock {
// Advanced GET configuration
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("item1", "item2"))))
)
}
// Advanced POST configuration
mockPostConfigure(
url = "/api/webhooks",
urlPatternFn = { urlEqualTo(it) }
) { builder, serde ->
builder
.withHeader("X-Webhook-Secret", equalTo("secret123"))
.withRequestBody(containing("event_type"))
.willReturn(
aResponse()
.withStatus(200)
.withBody("Webhook received")
)
}
}
}
Behavioral Mocking¶
Simulate service behavior changes over multiple calls:
test("service recovers from failure") {
TestSystem.validate {
wiremock {
behaviourFor("/api/external-service", WireMock::get) {
initially {
aResponse()
.withStatus(503)
.withBody("Service unavailable")
}
then {
aResponse()
.withStatus(503)
.withBody("Still unavailable")
}
then {
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(it.serialize(ServiceResponse(status = "OK")))
}
}
}
http {
// First call - failure
getResponse("/api/external-service") { response ->
response.status shouldBe 503
}
// Second call - still failing
getResponse("/api/external-service") { response ->
response.status shouldBe 503
}
// Third call - success
get<ServiceResponse>("/api/external-service") { response ->
response.status shouldBe "OK"
}
}
}
}
Testing Circuit Breaker¶
Test circuit breaker patterns with WireMock:
test("circuit breaker opens after failures") {
TestSystem.validate {
wiremock {
// Mock service that fails
mockGet(
url = "/api/unreliable-service",
statusCode = 500,
responseBody = "Internal Server Error".some()
)
}
// Application calls the service multiple times
repeat(5) {
http {
getResponse("/api/call-external") { response ->
// First few calls fail
response.status shouldBe 500
}
}
}
// Update mock to return success
wiremock {
mockGet(
url = "/api/unreliable-service",
statusCode = 200,
responseBody = ServiceResponse(status = "OK").some()
)
}
// Circuit breaker should open, need to wait for recovery
delay(5.seconds)
http {
get<ServiceResponse>("/api/call-external") { response ->
response.status shouldBe "OK"
}
}
}
}
Complete Example¶
Here's a complete test with multiple external service mocks:
test("should create order with external service validation") {
TestSystem.validate {
val userId = "user-123"
val productId = "product-456"
val categoryId = 1
// Mock user service
wiremock {
mockGet(
url = "/users/$userId",
statusCode = 200,
responseBody = User(id = userId, name = "John Doe", active = true).some(),
responseHeaders = mapOf("X-Service" to "UserService")
)
}
// Mock product catalog service
wiremock {
mockGet(
url = "/products/$productId",
statusCode = 200,
responseBody = Product(
id = productId,
name = "Laptop",
price = 999.99,
stock = 10
).some()
)
}
// Mock category service
wiremock {
mockGet(
url = "/categories/$categoryId",
statusCode = 200,
responseBody = Category(id = categoryId, name = "Electronics", active = true).some()
)
}
// Mock inventory service (POST to reserve stock)
wiremock {
mockPost(
url = "/inventory/reserve",
statusCode = 200,
requestBody = ReserveStockRequest(productId = productId, quantity = 1).some(),
responseBody = ReservationResponse(reservationId = "res-789", success = true).some()
)
}
// Create order via your API
http {
postAndExpectBody<OrderResponse>(
uri = "/orders",
body = CreateOrderRequest(
userId = userId,
productId = productId,
quantity = 1
).some()
) { response ->
response.status shouldBe 201
response.body().orderId shouldNotBe null
response.body().status shouldBe "CREATED"
}
}
// Verify order was stored
postgresql {
shouldQuery<Order>(
"SELECT * FROM orders WHERE user_id = ?",
mapper = { row ->
Order(
id = row.long("id"),
userId = row.string("user_id"),
productId = row.string("product_id"),
quantity = row.int("quantity")
)
}
) { orders ->
orders.size shouldBe 1
orders.first().userId shouldBe userId
orders.first().productId shouldBe productId
}
}
// Verify event was published
kafka {
shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {
actual.userId == userId &&
actual.productId == productId
}
}
}
}
Error Scenarios¶
Test how your application handles external service failures:
test("should handle external service unavailability") {
TestSystem.validate {
// Mock external service returning 503
wiremock {
mockGet(
url = "/external-api/data",
statusCode = 503,
responseBody = ErrorResponse("Service temporarily unavailable").some()
)
}
// Your application should handle this gracefully
http {
getResponse("/api/fetch-data") { response ->
response.status shouldBe 503 // or your fallback status
}
}
}
}
test("should handle timeout") {
TestSystem.validate {
wiremock {
mockGetConfigure("/slow-endpoint") { builder, _ ->
builder.willReturn(
aResponse()
.withStatus(200)
.withBody("Response")
.withFixedDelay(5000) // 5 second delay
)
}
}
http {
getResponse("/api/call-slow-service") { response ->
// Your application should timeout and handle it
response.status shouldBe 504 // Gateway timeout
}
}
}
}
Integration Testing¶
Test complex integrations with multiple services:
test("should orchestrate multiple services") {
TestSystem.validate {
val userId = "user-123"
// Mock authentication service
wiremock {
mockPost(
url = "/auth/validate",
statusCode = 200,
requestBody = TokenRequest(token = "jwt-token").some(),
responseBody = TokenValidation(valid = true, userId = userId).some()
)
}
// Mock permissions service
wiremock {
mockGet(
url = "/permissions/$userId",
statusCode = 200,
responseBody = Permissions(
userId = userId,
roles = listOf("USER", "ADMIN")
).some()
)
}
// Make authenticated request
http {
get<SecureData>(
uri = "/api/secure-data",
token = "jwt-token".some()
) { data ->
data.accessible shouldBe true
}
}
}
}
Request Verification¶
Verify that requests were made as expected:
test("should verify request details") {
TestSystem.validate {
wiremock {
mockPost(
url = "/api/webhook",
statusCode = 200,
metadata = mapOf(
"X-Signature" to "expected-signature"
)
)
}
// Trigger webhook
http {
postAndExpectBodilessResponse(
uri = "/trigger-webhook",
body = WebhookTrigger(event = "user.created").some()
) { response ->
response.status shouldBe 200
}
}
// Verify the webhook was called with correct signature
// (WireMock will only match if headers match)
}
}