Stub External HTTP APIs with WireMock in Java
Use WireMock in Java tests to stub external HTTP services. Covers response templating, delay simulation, stateful stubs, and request verification.
WireMock is a Java library that starts a real HTTP server inside the test process. Instead of calling a real third-party API, the code under test calls WireMock, which returns the responses you define. That includes status codes, headers, body, delays, and even stateful behavior — all without leaving the test.
When to Use
- Testing code that talks to an external REST API — payment gateways, SMS providers, email services, and the like — without depending on the real service.
- Simulating failures such as timeouts, 500s, slow responses, or rate limits to exercise retry and fallback logic.
- Verifying the exact request your code sends, not just the response it receives.
- Building a webhook receiver and needing a fake sender without setting up a real one.
- Running integration tests in CI where network access isn’t guaranteed.
When NOT to Use
- Testing your own endpoints. For Spring, use
MockMvcorWebTestClient. - Pure business-logic unit tests. Mock the interface directly with Mockito.
- Load testing. WireMock has its own overhead, so use a real test environment.
- Database interactions. Use Testcontainers with a real database instead.
Solution
Setup with JUnit 5
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>3.9.1</version>
<scope>test</scope>
</dependency>
Basic stub with the JUnit 5 extension
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ExternalServiceTest {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@Test
void shouldStubGetRequest() throws Exception {
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/users/1"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":1,\"name\":\"Alice\"}")));
HttpResponse<String> response = HttpClient.newHttpClient()
.send(HttpRequest.newBuilder()
.uri(URI.create(wireMock.getRuntimeInfo().getHttpBaseUrl() + "/api/users/1"))
.GET().build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertEquals("Alice", parseJson(response.body(), "name"));
}
}
This test starts WireMock on a random port, registers a stub for /api/users/1, then makes a real
HTTP call to it.
Stub with a JSON body from a file
wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo("/api/products"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withBodyFile("products-response.json")));
Put products-response.json under src/test/resources/__files/.
Simulate delay and timeout
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/slow"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withFixedDelay(5000)
.withBody("{\"data\":\"delayed\"}")));
Simulate error responses
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/error"))
.willReturn(WireMock.aResponse()
.withStatus(500)
.withBody("{\"error\":\"Internal Server Error\"}")));
wireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/api/rate-limited"))
.willReturn(WireMock.aResponse()
.withStatus(429)
.withHeader("Retry-After", "60")
.withBody("{\"error\":\"Rate limit exceeded\"}")));
Verify requests were made
wireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/api/orders"))
.withRequestBody(WireMock.matchingJsonPath("$.product_id", WireMock.equalTo("10")))
.withHeader("Authorization", WireMock.matching("Bearer .*")));
Stateful stubbing with scenarios
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/order/status"))
.inScenario("Order Processing")
.whenScenarioStateIs("Started")
.willReturn(WireMock.aResponse().withBody("{\"status\":\"pending\"}"))
.willSetStateTo("Processing"));
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/order/status"))
.inScenario("Order Processing")
.whenScenarioStateIs("Processing")
.willReturn(WireMock.aResponse().withBody("{\"status\":\"shipped\"}"))
.willSetStateTo("Completed"));
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/order/status"))
.inScenario("Order Processing")
.whenScenarioStateIs("Completed")
.willReturn(WireMock.aResponse().withBody("{\"status\":\"delivered\"}")));
Response templating with Handlebars
wireMock.stubFor(WireMock.get(WireMock.urlPathMatching("/api/users/([0-9]+)"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withTransformers("response-template")
.withBody("{\"id\":{{request.path.[1]}},\"name\":\"User {{request.path.[1]}}\"}")));
WireMock as a standalone server
import com.github.tomakehurst.wiremock.WireMockServer;
WireMockServer server = new WireMockServer(8089);
server.start();
server.stubFor(WireMock.get("/api/test").willReturn(WireMock.ok("hello")));
// ... run tests ...
server.stop();
Variants
Using the @WireMockTest annotation (WireMock 3+)
@WireMockTest(httpPort = 8089)
class AnnotationTest {
@Test
void testWithAnnotation(WireMock wireMock) {
wireMock.register(WireMock.get("/api/test")
.willReturn(WireMock.ok("hello")));
// test code
}
}
WireMock with Spring Boot
@SpringBootTest
class SpringIntegrationTest {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("external.api.url", wireMock::getRuntimeInfo);
}
}
Best Practices
- Let WireMock pick the port with
dynamicPort(). Hard-coded ports are a recipe for conflicts when tests run in parallel. - Store large response bodies in files under
__files/instead of inlining long JSON strings in Java. - Use
verify()to confirm your code sent the right request, not only that it got a response. - Reset WireMock between tests with
wireMock.resetAll()or the@ResetWireMockannotation. - Use response templating for dynamic IDs so you don’t need one stub per value.
- Always exercise the failure paths — delays, errors, and slow responses — or your retry and fallback logic stays untested.
Common Mistakes
- Using fixed ports — port 8080 might already be in use. Use
dynamicPort(). - Not resetting between tests — stubs from one test can leak into the next. Call
resetAll()in@AfterEach. - Stubbing too broadly —
urlMatching(".*")catches every request and hides missing stubs. - Not verifying requests — stubbing responses without checking the request misses bugs in how your code calls the API.
- Ignoring WireMock logs — enable verbose output with
.notifier(new ConsoleNotifier(true))when a stub isn’t matching.
Frequently Asked Questions
How do I match request bodies with JSON path?
wireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/api/orders"))
.withRequestBody(WireMock.matchingJsonPath("$.total", WireMockGreaterThan.greaterThan(100)))
.willReturn(WireMock.ok()));
Can WireMock proxy requests to a real server?
Yes. Proxy mode passes through any request that doesn't have a stub:
wireMock.stubFor(WireMock.any(WireMock.anyUrl())
.willReturn(WireMock.aResponse().proxiedFrom("https://real-api.example.com")));
How do I simulate a connection reset?
wireMock.stubFor(WireMock.get("/api/down")
.willReturn(WireMock.aResponse()
.withFault(Fault.CONNECTION_RESET_BY_PEER)));
Can I use WireMock with Kotlin?
Yes — the API is the same. In Kotlin, use a companion object for the extension:
companion object {
@RegisterExtension
@JvmStatic
val wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build()
}
How do I debug why a stub isn't matching?
Enable console logging and WireMock prints every incoming request, plus the stubs it tried to match:
wireMockConfig()
.notifier(new ConsoleNotifier(true))
Related Resources
Java Testcontainers Integration Tests
How to use Testcontainers in JUnit5 to spin up real Postgres, Redis, and Kafka containers for integration tests that are reliable and reproducible.
RecipeJUnit5 Soft Assertions with AssertJ
How to use AssertJ soft assertions in JUnit5 to collect multiple assertion failures in a single test instead of stopping at the first failure.
RecipeDesign Effective Integration Tests for Reliable Systems
How to write integration tests that verify component interactions using test containers, API contracts, consumer-driven contracts, and contract testing in Java, TypeScript, and Python.
RecipeMock Network Requests with MSW
How to use Mock Service Worker (MSW) to intercept network requests in JavaScript tests and development, including REST and GraphQL mocking.
RecipeAPI Mocking for Testing
Build reliable tests by mocking external APIs with WireMock, MockServer, and MSW to eliminate flakiness and test edge cases.
RecipeSetup Test Fixtures
How to manage test fixtures with factory patterns, setup/teardown hooks, and deterministic data for reliable unit and integration tests across Python, JavaScript, and Java.