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?

Why WireMock is Interview-Critical

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.

Advertisement

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'}}"
}
WireMock Interview Q&A

"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

What is a Webhook?

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.