Stubbing external HTTP APIs with WireMock in Java tests
External APIs are often the slowest and least predictable part of a Java test suite. A provider sandbox may be slow, rate-limited, unavailable outside business hours, or changed without notice. WireMock replaces that dependency with a controllable HTTP server on the test machine, allowing deliberate responses and repeatable assertions. This is especially useful for Australian teams integrating local banks, energy retailers, and government services, where sandbox access may require credentials or business-hours approval.
The aim is not to reproduce a provider’s entire system. It is to test the boundary your application depends on: the URL, method, headers, payload, status code, and response body. A well-designed stub makes those expectations visible, protects the pipeline from upstream variability, and gives failures a clear place to appear rather than burying them in a network timeout.
Establish a local HTTP boundary
WireMock is more than a response queue. It runs as a real HTTP server, maintains named stub mappings, records incoming requests, and can serve stateful scenarios. A JUnit 5 extension is usually the simplest starting point when the client can accept a base URL:
@RegisterExtension
static final WireMockExtension HTTP = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
The dynamic port avoids collisions with a developer’s local service or another test process. Construct the production client with wireMock.baseUrl() rather than hard-coding a host, and keep request verification in the test. This lets the same client run against WireMock, a provider sandbox, or a locally running service without rewriting its transport logic.
That boundary should be treated as test infrastructure, not hidden inside the client. This companion read on external API stubbing offers another perspective, but the practical rule is to keep each stub close to the test that relies on it and reuse a common builder only when the contract is genuinely shared.
Match the contract, not incidental traffic
A stub is useful when it fails for the right reason. Match the business-relevant request fields first, then relax fields that are incidental to the assertion. WireMock can match paths, methods, query parameters, headers, cookies, and JSON bodies:
HTTP.stubFor(post("/v1/payments")
.withHeader("Authorization", matching("Bearer .+"))
.withQueryParam("currency", equalTo("AUD"))
.withRequestBody(equalToJson("""
{"amount":125.50,"currency":"AUD"}
"""))
.willReturn(aResponse()
.withStatus(201)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":\"pay_123\",\"status\":\"accepted\"}")));
Exact matching is appropriate for a currency or endpoint version, while a regular expression suits an opaque payment identifier. matchingJsonPath can verify one property without making the whole body fragile, and absent can confirm that an optional header was not sent. The more specific the contract, the less likely a test will pass because an unrelated request happened to reach the same server.
Use scenarios when later requests depend on earlier state. A create operation might move a scenario from submitted to accepted, while a status request returns pending only in the first state. Define the transition explicitly with willSetStateTo, then assert both the transition and the final payload. This models a real asynchronous exchange without coupling the test to WireMock’s internal implementation.
Simulate failures and changing data
Success responses cover only part of a distributed integration. WireMock can also return 429 with Retry-After, emit 500 or 503, provide malformed JSON, delay a body, or close the connection unexpectedly. Fixed and chunked delays help exercise client timeouts, while fault injection reveals whether retries, backoff, and circuit breakers behave as intended. Keep delays bounded and deterministic so the suite remains fast and repeatable.
A resilience test should assert the observable outcome: one retry after a 429, a fallback after repeated 503 responses, or no second call after a circuit opens. Avoid sleeping for the full backoff period; inject or configure the delay instead. A real provider sandbox remains valuable for discovering behaviour that the local contract misses, but it should confirm the boundary rather than determine every test’s pass or fail.
Asynchronous workflows need a different fixture from a simple timeout. Model a request that returns 202 and an operation ID, then use scenario states to move the operation from pending to completed. Local data also needs honest detail: an Australian fixture may require AUD, a GST-inclusive total, and a state identifier such as NSW or VIC. For timestamps, use an IANA zone such as Australia/Sydney and retain the offset; hard-coding AEST or AEDT creates surprises when daylight-saving rules change.
Keep isolation across local and CI runs
Give each test class or test a clean stub set, and reset mappings and scenarios when their lifetime ends. Dynamic ports prevent port clashes, while reset operations prevent one test’s scenario state from altering another. Parallel execution also needs independent fixture data: two tests should not share mutable customer or order identifiers merely because they run at the same time. A shared server is possible, but it requires strict ownership and reset rules.
Pin the same WireMock version in Maven or Gradle and in any container used by CI. Teams split across Sydney and Melbourne can then get the same behaviour from laptops, an Australian cloud region, and the shared build farm. Containerised WireMock is useful for testing proxy and networking settings, while the embedded server is usually quicker for ordinary unit and integration tests. Keep provider credentials out of stub definitions and scrub request journals before publishing CI artefacts.
Local corporate proxies deserve special attention. Point clients at the local WireMock URL and ensure localhost bypasses the proxy, rather than weakening TLS or disabling proxy settings for the whole JVM. When a request fails, inspect WireMock’s request journal and unmatched requests before changing the client. This keeps debugging focused and avoids a spot of yak-shaving: the pipeline should stay green because the boundary behaves as specified, not because a real service happened to cooperate.
Choose the boundary and make it deterministic
Several tools can stand in for an external service, but their strengths differ:
| Approach | Strongest feature | Main limitation | Best fit |
|---|---|---|---|
| WireMock | Expressive matching, scenarios, fault injection, and request journals | Requires careful fixture design | Contract-level tests for Java services |
| MockWebServer | Small footprint and fast HTTP response queues | Limited domain scenarios and fault modelling | Low-level client and OkHttp tests |
| MockServer | Scripted responses and broader protocol support | More concepts and configuration | Mixed-protocol integration environments |
| Provider sandbox | Highest fidelity to the real implementation | Availability, credentials, rate limits, and shared state | Final checks around a controlled contract |
WireMock is a strong default when the test needs realistic HTTP behaviour, state transitions, or deliberate failure. MockWebServer can be enough for a tiny client, while MockServer may suit a wider integration platform. A provider sandbox belongs in a smaller verification layer because it introduces availability and data-state dependencies back into the suite.
Keep the first fixture narrow: one endpoint, one successful response, and one request assertion. Add failure and asynchronous scenarios only where the application has meaningful behaviour to verify. The next concrete step is to add one WireMock-backed test to the current build that stubs the riskiest external call, asserts both the outgoing request and the client’s response handling, and keeps the fixture beside that test.