WireMock is a mock HTTP server for tests. It lets you test your service when a dependency (a payment gateway, an SMS provider, another team's API) is unavailable, slow, expensive or hard to put into an error state: you define what each call should return and verify what your code sent. Try stubs visually in the WireMock visualizer.
Why Mock APIs?
WireMock lets you stub/mock third-party or dependent APIs so your tests run independently.
Without WireMock: your tests fail when Stripe, Razorpay, or any dependency is down.
With WireMock: your tests always run, always pass reliably — zero dependency on live services.
Asked at: Razorpay, PayU, CRED, Swiggy, Flipkart — any company with external integrations.
Maven Dependency
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.5.2</version>
<scope>test</scope>
</dependency>
Setup and Basic Stubs
// ── Embedded WireMock (spins up in-process — fastest for unit/integration tests) ──
@ExtendWith(WireMockExtension.class)
public class PaymentServiceTest {
@RegisterExtension
static WireMockExtension paymentGatewayMock = WireMockExtension.newInstance()
.options(wireMockConfig().port(9090)) // stub listens on port 9090
.build();
@BeforeEach
void stubPaymentGateway() {
// ── Stub 1: Successful payment ─────────────────────────────────
paymentGatewayMock.stubFor(
post(urlEqualTo("/payment/charge"))
.withHeader("Content-Type", containing("application/json"))
.withHeader("X-API-Key", matching(".+")) // any non-empty key
.withRequestBody(matchingJsonPath("$.amount", matching("[0-9]+")))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"transactionId": "TXN-12345",
"status": "SUCCESS",
"amount": 1500,
"currency": "INR"
}
""")
)
);
// ── Stub 2: Payment declined ────────────────────────────────────
paymentGatewayMock.stubFor(
post(urlEqualTo("/payment/charge"))
.withRequestBody(matchingJsonPath("$.cardNumber", equalTo("4000000000000002")))
.willReturn(aResponse()
.withStatus(402)
.withBody("""{"error":"CARD_DECLINED","message":"Insufficient funds"}""")
)
);
// ── Stub 3: Gateway timeout simulation ─────────────────────────
paymentGatewayMock.stubFor(
post(urlPathMatching("/payment/timeout/.*"))
.willReturn(aResponse()
.withFixedDelay(5000) // simulate 5-second delay
.withStatus(504)
)
);
// ── Stub 4: Fault injection (connection reset) ─────────────────
paymentGatewayMock.stubFor(
post(urlEqualTo("/payment/fault"))
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))
);
}
@Test
void processPayment_success_returnsTransactionId() {
PaymentService service = new PaymentService("http://localhost:9090");
PaymentResult result = service.charge(1500, "INR", "4111111111111111");
assertThat(result.getStatus()) .isEqualTo("SUCCESS");
assertThat(result.getTransactionId()) .isEqualTo("TXN-12345");
}
@Test
void processPayment_cardDeclined_throwsPaymentException() {
PaymentService service = new PaymentService("http://localhost:9090");
assertThatThrownBy(() -> service.charge(1500, "INR", "4000000000000002"))
.isInstanceOf(PaymentDeclinedException.class)
.hasMessageContaining("CARD_DECLINED");
}
@Test
void processPayment_gatewayTimeout_throwsTimeoutException() {
PaymentService service = new PaymentService("http://localhost:9090");
assertThatThrownBy(() -> service.chargeWithTimeout())
.isInstanceOf(GatewayTimeoutException.class);
}
// ── Verify WireMock received expected requests ─────────────────────
@Test
void processPayment_sendsCorrectHeadersToGateway() {
new PaymentService("http://localhost:9090").charge(500, "INR", "4111111111111111");
paymentGatewayMock.verify(1, postRequestedFor(urlEqualTo("/payment/charge"))
.withHeader("X-API-Key", matching(".+"))
.withHeader("Content-Type", containing("application/json"))
.withRequestBody(matchingJsonPath("$.amount", equalTo("500")))
);
}
}
Running WireMock in Docker for CI
# docker-compose.yml — WireMock as a standalone container
services:
wiremock:
image: wiremock/wiremock:3.5.2
container_name: payment-gateway-mock
ports: [ "9090:8080" ]
volumes:
- ./wiremock-stubs:/home/wiremock # mount stub JSON files
command: --verbose --global-response-templating
# wiremock-stubs/mappings/payment-success.json
{
"request": { "method": "POST", "url": "/payment/charge" },
"response": {
"status": 200,
"jsonBody": {
"transactionId": "{{randomValue type='UUID'}}",
"status": "SUCCESS",
"amount": "{{jsonPath request.body '$.amount'}}"
},
"headers": { "Content-Type": "application/json" }
}
}
Response Templating
// Dynamic response using request values (Handlebars templating)
paymentGatewayMock.stubFor(
post(urlEqualTo("/payment/charge"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBodyFile("templates/payment-response.json") // file from __files/
.withTransformers("response-template") // enable templating
)
);
// payment-response.json template:
{
"transactionId": "TXN-{{randomValue type='ALPHANUMERIC' length=8}}",
"status": "SUCCESS",
"amount": {{jsonPath request.body '$.amount'}},
"currency": "{{jsonPath request.body '$.currency'}}",
"timestamp": "{{now format='yyyy-MM-dd'T'HH:mm:ssZ'}}"
}
"What is the difference between a mock and a stub?"
Stub: returns pre-defined responses regardless of how it was called.
Mock: verifies the interaction happened (how many times, with what params).
WireMock does BOTH — use stubFor() for stubs, verify() for mock assertions.
"When would you use WireMock over Testcontainers?"
WireMock: mock HTTP APIs you do not control (Stripe, Razorpay, SMS gateway).
Testcontainers: spin up real databases, Kafka, Redis — your own infrastructure.
Testing Webhooks
A webhook is a reverse API — instead of YOU calling the server, the SERVER calls YOU.
Example: Stripe calls your endpoint when a payment succeeds/fails.
Example: GitHub calls your endpoint when a PR is opened.
Challenge: You cannot just send a request and check a response.
You must SET UP a receiver, TRIGGER the event, then VERIFY the webhook was received correctly.
WireMock as a Webhook Receiver
// WireMock acts as your webhook receiver endpoint
public class WebhookTest {
@RegisterExtension
static WireMockExtension webhookReceiver = WireMockExtension.newInstance()
.options(wireMockConfig().port(8090))
.build();
@BeforeEach
void setupWebhookEndpoint() {
// Tell WireMock to accept POST to /webhooks/payment
webhookReceiver.stubFor(
post(urlEqualTo("/webhooks/payment"))
.willReturn(aResponse().withStatus(200).withBody("OK"))
);
}
@Test
void paymentWebhook_onSuccess_sentWithCorrectPayload() throws InterruptedException {
// Step 1: Register webhook URL with the system
given().spec(withAuth("admin"))
.body(Map.of(
"url", "http://localhost:8090/webhooks/payment",
"events", List.of("payment.success", "payment.failed"),
"secret", "webhook-secret-key"
))
.when().post("/api/webhooks")
.then().statusCode(201);
// Step 2: Trigger a payment event
given().spec(withAuth("customer"))
.body(Map.of("amount", 1500, "currency", "INR", "method", "UPI"))
.when().post("/api/payments")
.then().statusCode(201);
// Step 3: Wait for webhook to be delivered (async)
Thread.sleep(2000); // or use Awaitility for better polling
// Step 4: Verify webhook was received
webhookReceiver.verify(1,
postRequestedFor(urlEqualTo("/webhooks/payment"))
.withHeader("Content-Type", containing("application/json"))
.withHeader("X-Webhook-Signature", matching(".+")) // HMAC signature
.withRequestBody(matchingJsonPath("$.event", equalTo("payment.success")))
.withRequestBody(matchingJsonPath("$.amount", equalTo("1500")))
);
}
@Test
void webhookSignature_isValidHMAC() throws Exception {
// Capture the webhook request
LoggedRequest webhookRequest = webhookReceiver
.findAll(postRequestedFor(urlEqualTo("/webhooks/payment")))
.get(0);
String signature = webhookRequest.getHeader("X-Webhook-Signature");
String body = webhookRequest.getBodyAsString();
String secret = "webhook-secret-key";
// Compute expected HMAC-SHA256 signature
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expectedSig = "sha256=" + Hex.encodeHexString(mac.doFinal(body.getBytes()));
assertThat(signature).isEqualTo(expectedSig);
}
}
Waiting for Async Events with Awaitility
// Awaitility: poll until condition is true (better than Thread.sleep)
// Dependency: org.awaitility:awaitility:4.2.1
@Test
void orderStatus_afterPayment_becomesConfirmedWithin30Seconds() {
// Trigger async payment
String orderId = given().spec(withAuth("customer"))
.body(Map.of("cartId", "CART-001", "paymentMethod", "UPI"))
.when().post("/api/orders")
.then().statusCode(201)
.extract().jsonPath().getString("id");
// Poll every 2 seconds for up to 30 seconds
await()
.atMost(30, TimeUnit.SECONDS)
.pollInterval(2, TimeUnit.SECONDS)
.pollDelay(2, TimeUnit.SECONDS) // first check after 2s
.untilAsserted(() -> {
given().spec(withAuth("customer"))
.pathParam("id", orderId)
.when().get("/api/orders/{id}")
.then()
.statusCode(200)
.body("status", equalTo("CONFIRMED")); // keeps retrying until this passes
});
}
Mocking Checklist
- Stub the error paths you can't easily trigger for real: 500, 503 with Retry-After, timeouts (
withFixedDelay), malformed JSON and empty bodies. - Verify the outgoing request too (URL, headers, body), not just that your code handled the stubbed response.
- Keep a few tests against the real dependency (or its sandbox) so the mocks don't drift from reality; contract tests help here.
- Reset stubs between tests so one test's setup can't leak into another.
FAQs
What is WireMock used for?
To simulate HTTP APIs in tests: return predefined responses, simulate errors and delays, and verify the requests your application sends, without depending on the real service.
How do you test a webhook?
Run a receiver such as WireMock at a URL the system can call, register that URL, trigger the event, then wait (with Awaitility) and verify the received request's headers, signature and body.
Should I use Thread.sleep() to wait for a webhook?
No. Poll with Awaitility until the condition is met or a timeout expires, which is faster when the event arrives quickly and clearer when it doesn't.