Skip to main content

WireMock โ€” Mocking External HTTP APIs

Who this guide is for

What is WireMock?โ€‹

Imagine your application calls an external Payment API. In production, you hit https://api.stripe.com. But in tests:

  • Stripe might be down โ†’ your tests fail for no reason
  • Stripe rate-limits you โ†’ your CI pipeline gets blocked
  • You can't easily make Stripe return a 500 error โ†’ you can't test error handling
  • Stripe charges money per call โ†’ testing gets expensive

WireMock solves this. It starts a real HTTP server locally that pretends to be Stripe. Your application talks to http://localhost:8089 instead, and WireMock responds with whatever you configure.

Production:
Your App โ”€โ”€HTTPโ”€โ”€โ–บ Stripe API (real)

Tests with WireMock:
Your App โ”€โ”€HTTPโ”€โ”€โ–บ WireMock (localhost:8089)
โ†“ returns pre-configured responses
{"status": "SUCCESS", "chargeId": "ch_123"}

WireMock vs Mockito โ€” When to Use Which?โ€‹

MockitoWireMock
What it mocksJava objects in memoryAn actual HTTP server
TestsUnit tests (business logic)Integration tests (HTTP clients)
ScopeSingle class, no networkFull HTTP request/response cycle
Catches bugs inLogic, conditions, calculationsSerialization, headers, status codes, retries, timeouts
Speedโšก Milliseconds๐Ÿข Seconds (starts HTTP server)
Rule of Thumb

Use Mockito when you want to test what your code does. Use WireMock when you want to test how your code talks to external services over HTTP.


๐Ÿš€ Getting Startedโ€‹

1. Add Dependenciesโ€‹

<!-- pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>

<!-- OR standalone WireMock -->
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.9.1</version>
<scope>test</scope>
</dependency>

2. Basic Setup with Spring Bootโ€‹

@SpringBootTest
@AutoConfigureWireMock(port = 0) // random port โ€” avoids conflicts in CI
class PaymentClientIntegrationTest {

// WireMock injects the random port into this property
@Value("${wiremock.server.port}")
private int wireMockPort;

@Autowired
private PaymentClient paymentClient;

@Test
void getPayment_success_returnsPayment() {
// Arrange โ€” tell WireMock what to respond
stubFor(get(urlEqualTo("/api/payments/PAY-123"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"id": "PAY-123",
"amount": 99.99,
"currency": "USD",
"status": "SUCCESS"
}
""")));

// Act โ€” your real HTTP client makes a real HTTP request
Payment payment = paymentClient.getPayment("PAY-123");

// Assert โ€” verify the response was correctly deserialized
assertEquals("PAY-123", payment.getId());
assertEquals(new BigDecimal("99.99"), payment.getAmount());
assertEquals("SUCCESS", payment.getStatus());

// Verify โ€” WireMock confirms the request was made correctly
verify(getRequestedFor(urlEqualTo("/api/payments/PAY-123"))
.withHeader("Accept", containing("application/json")));
}
}

3. Configure Your Client to Use WireMockโ€‹

# application-test.yml (or application.yml with test profile)
payment:
api:
base-url: http://localhost:${wiremock.server.port}
@Configuration
public class PaymentClientConfig {

@Bean
public PaymentClient paymentClient(
@Value("${payment.api.base-url}") String baseUrl,
RestClient.Builder builder) {
return new PaymentClient(builder.baseUrl(baseUrl).build());
}
}

๐Ÿง  Deep Dive: Stub Matchingโ€‹

WireMock matches incoming requests against stubs using configurable matchers. Understanding these is key to writing robust tests.

URL Matchingโ€‹

// Exact match
stubFor(get(urlEqualTo("/api/users/123")));

// Path pattern (regex)
stubFor(get(urlPathMatching("/api/users/[0-9]+")));

// Path with any query params
stubFor(get(urlPathEqualTo("/api/users")));

Header Matchingโ€‹

stubFor(get(urlEqualTo("/api/secure"))
.withHeader("Authorization", equalTo("Bearer abc123"))
.withHeader("Accept", containing("application/json"))
.withHeader("X-Request-Id", matching("[a-f0-9-]{36}")) // UUID pattern
.willReturn(ok()));

Query Parameter Matchingโ€‹

stubFor(get(urlPathEqualTo("/api/products"))
.withQueryParam("category", equalTo("laptop"))
.withQueryParam("minPrice", matching("[0-9]+"))
.withQueryParam("page", absent()) // must NOT be present
.willReturn(okJson("""
{"products": [], "total": 0}
""")));

Request Body Matching (POST/PUT)โ€‹

// Exact JSON body match
stubFor(post(urlEqualTo("/api/orders"))
.withRequestBody(equalToJson("""
{"productId": "PROD-1", "quantity": 2}
"""))
.willReturn(created()));

// Partial JSON match โ€” only check specific fields
stubFor(post(urlEqualTo("/api/orders"))
.withRequestBody(matchingJsonPath("$.productId", equalTo("PROD-1")))
.withRequestBody(matchingJsonPath("$.quantity", matching("[0-9]+")))
.willReturn(created()));

// JSON Schema validation
stubFor(post(urlEqualTo("/api/orders"))
.withRequestBody(matchingJsonSchema("""
{
"type": "object",
"required": ["productId", "quantity"],
"properties": {
"productId": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1}
}
}
"""))
.willReturn(created()));

Response Helpersโ€‹

// Common response shortcuts
stubFor(get("/ok").willReturn(ok())); // 200
stubFor(get("/json").willReturn(okJson("{}"))); // 200 + JSON
stubFor(get("/created").willReturn(created())); // 201
stubFor(get("/bad").willReturn(badRequest())); // 400
stubFor(get("/forbidden").willReturn(forbidden())); // 403
stubFor(get("/notfound").willReturn(notFound())); // 404
stubFor(get("/error").willReturn(serverError())); // 500

// Full control
stubFor(get("/custom").willReturn(aResponse()
.withStatus(429)
.withHeader("Retry-After", "30")
.withBody("Rate limit exceeded")));

๐Ÿ’ฅ Fault Injection & Resilience Testingโ€‹

This is where WireMock truly shines โ€” testing what happens when things go wrong.

Simulating Delays (Timeout Testing)โ€‹

@Test
void getPayment_timeout_throwsException() {
// Simulate a 5-second delay โ€” your client has a 2-second timeout
stubFor(get(urlEqualTo("/api/payments/PAY-123"))
.willReturn(ok()
.withFixedDelay(5000))); // 5 seconds

// Your client should time out and throw
assertThrows(PaymentServiceTimeoutException.class,
() -> paymentClient.getPayment("PAY-123"));
}

// Random delay โ€” simulate real-world latency variance
stubFor(get("/api/slow")
.willReturn(ok()
.withUniformRandomDelay(200, 2000))); // 200ms to 2s

Simulating Network Faultsโ€‹

// Connection reset โ€” TCP connection drops mid-response
stubFor(get("/api/unstable")
.willReturn(aResponse()
.withFault(Fault.CONNECTION_RESET_BY_PEER)));

// Empty response โ€” server closes connection without sending anything
stubFor(get("/api/empty")
.willReturn(aResponse()
.withFault(Fault.EMPTY_RESPONSE)));

// Malformed response โ€” garbage bytes
stubFor(get("/api/garbage")
.willReturn(aResponse()
.withFault(Fault.RANDOM_DATA_THEN_CLOSE)));

// Partial response โ€” sends headers, then drops connection
stubFor(get("/api/partial")
.willReturn(aResponse()
.withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
Fault TypeWhat It SimulatesTests Your...
FIXED_DELAYSlow APITimeout handling, circuit breakers
CONNECTION_RESET_BY_PEERNetwork failureRetry logic, fallback behavior
EMPTY_RESPONSEServer crash mid-requestError parsing, null safety
RANDOM_DATA_THEN_CLOSECorrupted responseDeserialization error handling

Testing Retry Logic with Sequential Responsesโ€‹

@Test
void getPayment_retriesOnFailure_succeedsOnThirdAttempt() {
// 1st call โ†’ 500
// 2nd call โ†’ 500
// 3rd call โ†’ 200 with success
stubFor(get(urlEqualTo("/api/payments/PAY-123"))
.inScenario("retry-test")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(serverError())
.willSetStateTo("SECOND_ATTEMPT"));

stubFor(get(urlEqualTo("/api/payments/PAY-123"))
.inScenario("retry-test")
.whenScenarioStateIs("SECOND_ATTEMPT")
.willReturn(serverError())
.willSetStateTo("THIRD_ATTEMPT"));

stubFor(get(urlEqualTo("/api/payments/PAY-123"))
.inScenario("retry-test")
.whenScenarioStateIs("THIRD_ATTEMPT")
.willReturn(okJson("""
{"id": "PAY-123", "status": "SUCCESS"}
""")));

// Act โ€” client should retry and eventually succeed
Payment payment = paymentClient.getPayment("PAY-123");

assertEquals("SUCCESS", payment.getStatus());

// Verify โ€” exactly 3 requests were made (2 retries + 1 success)
verify(3, getRequestedFor(urlEqualTo("/api/payments/PAY-123")));
}

Testing Rate Limiting (429 Responses)โ€‹

@Test
void getPayment_rateLimited_waitsAndRetries() {
stubFor(get(urlEqualTo("/api/payments/PAY-123"))
.inScenario("rate-limit")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(aResponse()
.withStatus(429)
.withHeader("Retry-After", "1"))
.willSetStateTo("ALLOWED"));

stubFor(get(urlEqualTo("/api/payments/PAY-123"))
.inScenario("rate-limit")
.whenScenarioStateIs("ALLOWED")
.willReturn(okJson("""
{"id": "PAY-123", "status": "SUCCESS"}
""")));

Payment payment = paymentClient.getPayment("PAY-123");
assertEquals("SUCCESS", payment.getStatus());
}

๐Ÿ”ฌ Advanced Patternsโ€‹

Response Templating โ€” Dynamic Responsesโ€‹

WireMock can generate responses based on the request data:

@RegisterExtension
static WireMockExtension wm = WireMockExtension.newInstance()
.options(wireMockConfig()
.dynamicPort()
.extensions(new ResponseTemplateTransformer(true)))
.build();

@Test
void echo_returnsRequestData() {
wm.stubFor(get(urlPathMatching("/api/users/(.+)"))
.willReturn(okJson("""
{
"requestedId": "{{request.pathSegments.[2]}}",
"timestamp": "{{now}}",
"method": "{{request.method}}"
}
""")
.withTransformers("response-template")));

// GET /api/users/alice โ†’
// {"requestedId": "alice", "timestamp": "2024-...", "method": "GET"}
}

Stateful Stubs โ€” Model API State Transitionsโ€‹

// Simulate: create โ†’ confirm โ†’ ship
@Test
void orderLifecycle_stateTransitions() {
// GET before creation โ†’ 404
stubFor(get("/api/orders/ORD-1")
.inScenario("order-lifecycle")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(notFound()));

// POST to create โ†’ returns PENDING, transitions state
stubFor(post("/api/orders")
.inScenario("order-lifecycle")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(okJson("""{"id":"ORD-1","status":"PENDING"}"""))
.willSetStateTo("CREATED"));

// GET after creation โ†’ returns PENDING
stubFor(get("/api/orders/ORD-1")
.inScenario("order-lifecycle")
.whenScenarioStateIs("CREATED")
.willReturn(okJson("""{"id":"ORD-1","status":"PENDING"}""")));
}

Loading Stubs from JSON Filesโ€‹

Instead of defining stubs in Java, you can load them from JSON files (useful for large or shared stubs):

src/test/resources/
โ””โ”€โ”€ __files/ โ† response body files
โ”‚ โ””โ”€โ”€ payment-success.json
โ””โ”€โ”€ mappings/ โ† stub definitions
โ””โ”€โ”€ get-payment.json
// mappings/get-payment.json
{
"request": {
"method": "GET",
"urlPattern": "/api/payments/[A-Z]+-[0-9]+"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"bodyFileName": "payment-success.json"
}
}
// __files/payment-success.json
{
"id": "PAY-123",
"amount": 99.99,
"currency": "USD",
"status": "SUCCESS"
}

๐Ÿค Contract Testing with WireMockโ€‹

When your service depends on another team's API, you need to ensure compatibility. Contract testing validates that the API you stub in tests matches the real API's behavior.

Without contract testing:
Your stub: GET /api/users โ†’ {"name": "Alice"}
Real API: GET /api/users โ†’ {"fullName": "Alice"} โ† Field renamed!
Your test passes โœ… but production breaks โŒ

With contract testing:
Provider publishes contract (e.g., via Pact / Spring Cloud Contract)
Your WireMock stubs are auto-generated FROM the contract
If provider changes the API โ†’ contract fails โ†’ you know before deploying

Spring Cloud Contract Integrationโ€‹

// Consumer side โ€” stubs are auto-downloaded from the provider's published contracts
@SpringBootTest
@AutoConfigureStubRunner(
ids = "com.example:payment-service:+:stubs:8089",
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
class PaymentClientContractTest {

@Autowired
private PaymentClient paymentClient;

@Test
void getPayment_matchesProviderContract() {
// Stubs are loaded from the payment-service's published contracts
// If the real API changes, the contract test will fail
Payment payment = paymentClient.getPayment("PAY-123");
assertNotNull(payment);
assertEquals("SUCCESS", payment.getStatus());
}
}

๐Ÿณ WireMock with Testcontainersโ€‹

For complete isolation (no port conflicts, consistent across local/CI):

@SpringBootTest
@Testcontainers
class PaymentClientDockerTest {

@Container
static WireMockContainer wiremock = new WireMockContainer("wiremock/wiremock:3.9.1")
.withMappingFromResource("mappings/get-payment.json")
.withFileFromResource("__files/payment-success.json");

@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("payment.api.base-url", wiremock::getBaseUrl);
}

@Autowired
private PaymentClient paymentClient;

@Test
void getPayment_fromDockerizedWireMock() {
Payment payment = paymentClient.getPayment("PAY-123");
assertEquals("SUCCESS", payment.getStatus());
}
}
ApproachStartupPort ConflictsCI Consistency
@AutoConfigureWireMock (in-process)โšก Fast (~200ms)Use port=0 for randomGood
Testcontainers WireMock๐Ÿข Slower (~2s)None (Docker)Best

โœ… Best Practicesโ€‹

PracticeWhy
Always use port = 0 (random port)Prevents port conflicts when running tests in parallel
Match only what mattersDon't assert on incidental headers or exact query param order โ€” tests become brittle
Reset stubs between testsUse @BeforeEach WireMock.reset() or @DirtiesContext to prevent stub leakage
Test failure scenarios firstTimeouts, 500s, malformed responses are where real bugs live
Use response files for large payloadsKeep test code clean; put 100-line JSON in __files/
Verify outbound requestsDon't just test responses โ€” verify your app sends correct headers, auth, and body
Name your stubsstubFor(...).withName("get-payment-success") โ€” easier debugging in logs

Interview Questionsโ€‹

For New Learnersโ€‹

Q: What is WireMock and why do we need it?

WireMock is a library that starts a real HTTP server locally to simulate external APIs in tests. We need it because real APIs may be unreliable, rate-limited, expensive, or unable to simulate error scenarios like timeouts and 500 errors.

Q: What is the difference between WireMock and Mockito?

Mockito mocks Java objects in memory โ€” used in unit tests for business logic. WireMock mocks an actual HTTP server โ€” used in integration tests for HTTP client code. WireMock catches bugs that Mockito cannot: serialization issues, incorrect headers, status code handling, and retry logic.

Q: What does @AutoConfigureWireMock(port = 0) do?

It starts a WireMock HTTP server on a randomly available port before the test class runs, and injects the port number via ${wiremock.server.port}. Using port 0 (random) prevents port conflicts when multiple test suites run in parallel.

For Senior Engineersโ€‹

Q: What failure scenarios should teams always simulate with WireMock?

At minimum: (1) Timeouts โ€” withFixedDelay() exceeding client timeout. (2) Throttling โ€” 429 with Retry-After header. (3) Server errors โ€” 500/503 to test circuit breakers. (4) Connection resets โ€” Fault.CONNECTION_RESET_BY_PEER for retry logic. (5) Malformed payloads โ€” invalid JSON to test deserialization error handling.

Q: How do you test retry and idempotency with WireMock?

Use WireMock Scenarios: define sequential responses (e.g., 500 โ†’ 500 โ†’ 200). Assert that the client retried the correct number of times with verify(3, getRequestedFor(...)). For idempotency, verify that a retry doesn't cause duplicate side effects โ€” check that the downstream system received exactly one successful request.

Q: How do you avoid brittle WireMock tests?

Match only contract-relevant fields: use matchingJsonPath instead of equalToJson for large payloads, use urlPathMatching instead of urlEqualTo when query params vary, and don't assert on incidental headers (like User-Agent). Focus stubs on the contract โ€” what your code depends on โ€” not on every detail of the response.

Q: What are the trade-offs between in-process WireMock and Testcontainers WireMock?

In-process (@AutoConfigureWireMock) is faster to start (~200ms) and simpler to configure, but runs inside the JVM โ€” sharing the same process as your tests. Testcontainers WireMock runs in Docker (~2s startup), providing complete process isolation and identical behavior across local and CI environments. Use in-process for most tests; use Testcontainers when you need strict environment consistency or are debugging network-level issues.

Q: How does contract testing with WireMock prevent production integration failures?

Without contracts, your WireMock stubs may diverge from the real API over time. With Spring Cloud Contract or Pact, the API provider publishes a contract (expected request/response pairs). Your stubs are auto-generated from this contract. If the provider changes the API in a breaking way, the contract test fails in CI โ€” before the change reaches production.