WireMock is an HTTP mock server for Java that answers requests with stubbed responses, so we can test code that calls a REST API without the real API. We define which requests it matches and what it returns, point our HTTP client at its URL, and verify afterwards which requests arrived.
We use WireMock in integration tests of services that depend on other services, such as a shop that calls a payment provider or a parcel tracking API. It also plays failures that are hard to get from a real API on demand, such as slow responses, dropped connections and error codes.
The following example starts WireMock 3 on a random port, stubs one endpoint, calls it with the JDK HttpClient, and counts the requests.
WireMockServer wireMock = new WireMockServer(options().dynamicPort());
wireMock.start();
wireMock.stubFor(get("/parcels/p1").willReturn(okJson("{\"status\":\"IN_TRANSIT\"}")));
ParcelClient client = new ParcelClient(wireMock.baseUrl(), Duration.ofSeconds(2));
String body = client.track("p1"); // "{"status":"IN_TRANSIT"}"
int calls = wireMock.countRequestsMatching(getRequestedFor(urlEqualTo("/parcels/p1")).build()).getCount(); // 1
wireMock.stop();
Notice that the client gets wireMock.baseUrl() instead of a hard-coded host, so the test never depends on a fixed port. We go through the Maven setup, the JUnit 5 integration, request matching, responses, delays and faults, verification, stateful scenarios and Spring Boot tests.
1. How WireMock Answers a Request
WireMock runs an embedded Jetty server inside the test JVM, or as a standalone process. Every incoming request is compared with the registered stubs, and the newest stub that matches wins. The response of that stub goes back to the client, and the request is stored in a journal that tests can query later.

A request that matches no stub gets a 404 response with a report of the closest stub and the field that did not match. The report is the fastest way to find a typo in a URL, a missing header or a body that differs by one field.
2. Maven Dependency for WireMock 3
WireMock 3 moved to the group org.wiremock. The old com.github.tomakehurst:wiremock-jre8 artifact belongs to WireMock 2 and is no longer updated.
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>3.13.2</version>
<scope>test</scope>
</dependency>
WireMock 3 requires Java 11 or later. The wiremock artifact brings Jetty 11 and Jackson as normal dependencies, which can clash with the versions of a Spring Boot application. In that case, use org.wiremock:wiremock-standalone, which shades its dependencies into one JAR. The examples use WireMock 3.13.2, JUnit 6.1.3 and Java 25. WireMock 4 is in beta at the time of writing, so we stay on the 3.x line.
3. Starting WireMock in JUnit Tests
WireMock ships a JUnit Jupiter extension that starts and stops the server for us. It works with JUnit 5 and JUnit 6, because both use the Jupiter API, and it comes in a declarative and a programmatic form.
3.1. Using @WireMockTest
The @WireMockTest annotation starts one server on a random port before the first test and stops it after the last one. It also resets stubs and the request journal before each test method, so tests cannot affect each other. A WireMockRuntimeInfo parameter gives the port and base URL, and the static DSL methods such as stubFor() talk to this server.
@WireMockTest
class ParcelClientTest {
@Test
void tracksParcel(WireMockRuntimeInfo wireMock) throws Exception {
stubFor(get("/parcels/p1").willReturn(okJson("{\"status\":\"IN_TRANSIT\"}")));
ParcelClient client = new ParcelClient(wireMock.getHttpBaseUrl(), Duration.ofSeconds(2));
assertEquals("{\"status\":\"IN_TRANSIT\"}", client.track("p1"));
verify(getRequestedFor(urlEqualTo("/parcels/p1")).withHeader("Accept", equalTo("application/json")));
}
}
Annotation attributes change the defaults. For example, @WireMockTest(httpPort = 8089) fixes the port, and httpsEnabled = true adds an HTTPS port with a self-signed certificate.
3.2. Using WireMockExtension
The programmatic extension fits when a test needs two mock APIs at once, or custom options such as a folder for response files. Each WireMockExtension field is its own server, and we call stubFor() on the field instead of the static DSL.
class ShippingTest {
@RegisterExtension
static WireMockExtension parcelApi = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@RegisterExtension
static WireMockExtension addressApi = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@Test
void usesTwoMockApis() throws Exception {
parcelApi.stubFor(get("/parcels/p1").willReturn(okJson("{\"status\":\"DELIVERED\"}")));
addressApi.stubFor(get("/addresses/a1").willReturn(okJson("{\"city\":\"Pune\"}")));
assertNotEquals(parcelApi.getPort(), addressApi.getPort());
assertEquals("{\"status\":\"DELIVERED\"}",
new ParcelClient(parcelApi.baseUrl(), Duration.ofSeconds(2)).track("p1"));
}
}
A static field keeps one server for all tests in the class, and an instance field starts a new server for each test. By default, a request that matches no stub only gets the 404 near-miss response, and the builder option failOnUnmatchedRequests(true) makes the extension fail the test as well.
3.3. Starting a WireMockServer by Hand
Outside of JUnit, or in a test framework without an extension, we create a WireMockServer, call start() and stop(), and reset it between tests with resetAll(), as the intro example does. A plain new WireMockServer() listens on port 8080, so pass options().dynamicPort() to avoid clashes. The static method WireMock.configureFor() only points the static DSL at a server and does not change the server’s port.
JUnit 4 projects use WireMockRule as a @Rule, which starts and stops the server by itself. Mixing the rule with JUnit 5 annotations such as @AfterEach does not work, because JUnit 5 ignores @Rule.
4. The Client Under Test
Say an online shop shows the delivery status of each order, and it reads the status from a carrier’s parcel tracking API. The shop wraps the HTTP call in a small client class, and the base URL comes from the constructor so a test can pass the WireMock URL.
class ParcelClient {
private final HttpClient http = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
private final String baseUrl;
private final Duration timeout;
ParcelClient(String baseUrl, Duration timeout) {
this.baseUrl = baseUrl;
this.timeout = timeout;
}
String track(String parcelId) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/parcels/" + parcelId))
.header("Accept", "application/json")
.timeout(timeout)
.GET()
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IllegalStateException("Tracking failed with status " + response.statusCode());
}
return response.body();
}
}
The request has a timeout, which is the setting the delay tests in section 7 check. Any client works the same way with WireMock, including Spring RestClient, OkHttp and Apache HttpClient.
5. Matching Requests
A stub has a request pattern and a response. The pattern matches on the HTTP method and the URL, and it can add conditions on query parameters, headers, cookies, authentication and the body. A request must meet every condition to match.
| URL matcher | Matches | Example |
|---|---|---|
| urlEqualTo | Path and query string, character by character | urlEqualTo(“/parcels?carrier=dhl”) |
| urlPathEqualTo | Path only, any query string | urlPathEqualTo(“/parcels”) |
| urlPathTemplate | Path with named parameters | urlPathTemplate(“/parcels/{id}”) |
| urlMatching / urlPathMatching | A regular expression | urlPathMatching(“/parcels/p[0-9]+”) |
The method shortcuts get(“/path”) and post(“/path”) use urlEqualTo, so a request with an extra query parameter does not match them. The next stub matches a JSON body by its fields, and the test sends a body with the fields in another order.
@Test
void matchesQueryHeaderAndJsonBody(WireMockRuntimeInfo wireMock) throws Exception {
stubFor(post(urlPathEqualTo("/parcels"))
.withQueryParam("carrier", equalTo("dhl"))
.withHeader("Content-Type", containing("json"))
.withRequestBody(equalToJson("{\"weightKg\": 2, \"city\": \"Pune\"}"))
.willReturn(created().withHeader("Location", "/parcels/p9")));
HttpRequest request = HttpRequest.newBuilder(URI.create(wireMock.getHttpBaseUrl() + "/parcels?carrier=dhl"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"city\": \"Pune\", \"weightKg\": 2}"))
.build();
HttpResponse<Void> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.discarding());
assertEquals(201, response.statusCode());
assertEquals("/parcels/p9", response.headers().firstValue("Location").orElseThrow());
}
The equalToJson() matcher ignores field order and spaces. For checks on a single field, matchingJsonPath(“$.city”, equalTo(“Pune”)) is less strict, and matchingXPath() does the same for XML bodies. When two stubs match, the newest one wins, unless we set atPriority(1), where a lower number means a higher priority.
6. Building Responses
The response side sets the status, headers and body. Shortcuts such as ok(), okJson(), created(), notFound() and serverError() cover the common cases, and aResponse() builds anything else.
@Test
void buildsResponses(WireMockRuntimeInfo wireMock) throws Exception {
stubFor(get("/parcels/p2").willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withHeader("Cache-Control", "no-store")
.withBody("{\"status\":\"DELIVERED\"}")));
stubFor(get(urlPathTemplate("/parcels/{id}")).atPriority(10)
.willReturn(okJson("{\"id\":\"{{request.path.id}}\",\"status\":\"IN_TRANSIT\"}")
.withTransformers("response-template")));
stubFor(get("/parcels/p0").willReturn(notFound()));
ParcelClient client = new ParcelClient(wireMock.getHttpBaseUrl(), Duration.ofSeconds(2));
assertEquals("{\"status\":\"DELIVERED\"}", client.track("p2"));
assertEquals("{\"id\":\"p77\",\"status\":\"IN_TRANSIT\"}", client.track("p77"));
assertThrows(IllegalStateException.class, () -> client.track("p0"));
}
The second stub uses response templating, which copies the path parameter id into the body with a Handlebars expression. Templating is active only for stubs with withTransformers(“response-template”), unless the server starts with globalTemplating(true). For large bodies, withBodyFile(“parcel.json”) reads the file from src/test/resources/__files.
7. Simulating Delays and Faults
Real APIs are slow during peak hours and drop connections during deployments. WireMock plays both cases, so we can check that our client gives up after its timeout and turns network errors into a clear message for the user.
WireMockServer wireMock = new WireMockServer(options().dynamicPort());
wireMock.start();
wireMock.stubFor(get("/parcels/slow").willReturn(okJson("{}").withFixedDelay(2000)));
wireMock.stubFor(get("/parcels/reset").willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
wireMock.stubFor(get("/parcels/garbage").willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
ParcelClient client = new ParcelClient(wireMock.baseUrl(), Duration.ofMillis(500));
String slow = client.track("slow"); // HttpTimeoutException: request timed out
String reset = client.track("reset"); // IOException: HTTP/1.1 header parser received no bytes
String garbage = client.track("garbage"); // IOException: Illegal character in chunk size
wireMock.stop();
The delay is 2 seconds and the client timeout is 500 ms, so the slow call fails after half a second instead of waiting for the response. Each fault produces an exception in the client.
| Setting | What WireMock does |
|---|---|
| withFixedDelay(ms) | Waits a fixed time before it sends the response |
| withUniformRandomDelay(min, max) / withLogNormalRandomDelay(median, sigma) | Waits a random time from the distribution |
| withChunkedDribbleDelay(chunks, totalMs) | Sends the body in pieces spread over the time |
| Fault.CONNECTION_RESET_BY_PEER | Closes the connection with a TCP reset |
| Fault.EMPTY_RESPONSE | Closes the connection without sending anything |
| Fault.MALFORMED_RESPONSE_CHUNK | Sends a 200 status followed by invalid chunked data |
| Fault.RANDOM_DATA_THEN_CLOSE | Sends random bytes and closes the connection |
Test retry logic with a 503 response followed by a 200, using the scenarios from section 9, and assert the number of calls with verify().
8. Verifying Requests
A stub proves that our code can handle a response. Verification proves that our code sent the right request, for example that it called the carrier only once or sent the API key. WireMock records every request, and verify() checks the journal against a pattern.
@Test
void verifiesRequests(WireMockRuntimeInfo wireMock) throws Exception {
stubFor(get(urlPathTemplate("/parcels/{id}")).willReturn(okJson("{\"status\":\"IN_TRANSIT\"}")));
ParcelClient client = new ParcelClient(wireMock.getHttpBaseUrl(), Duration.ofSeconds(2));
client.track("p1");
client.track("p1");
client.track("p2");
verify(2, getRequestedFor(urlEqualTo("/parcels/p1")));
verify(moreThanOrExactly(3), getRequestedFor(urlPathMatching("/parcels/.*")));
verify(0, postRequestedFor(anyUrl()));
List<LoggedRequest> requests = findAll(getRequestedFor(urlEqualTo("/parcels/p2")));
assertEquals("application/json", requests.get(0).getHeader("Accept"));
}
Verify only the requests that matter for the behavior under test, because a test that checks every call breaks on harmless refactorings. Count checks also come as exactly(), lessThan(), lessThanOrExactly() and moreThan().
9. Stateful Behavior with Scenarios
Some flows need a different answer for the same request, such as a parcel that is in transit on the first call and delivered on the next. A scenario is a named state machine. Each stub belongs to a state, and a stub can move the scenario into a new state after it answers.
@Test
void parcelGetsDelivered(WireMockRuntimeInfo wireMock) throws Exception {
stubFor(get("/parcels/p5").inScenario("delivery")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(okJson("{\"status\":\"IN_TRANSIT\"}"))
.willSetStateTo("DELIVERED"));
stubFor(get("/parcels/p5").inScenario("delivery")
.whenScenarioStateIs("DELIVERED")
.willReturn(okJson("{\"status\":\"DELIVERED\"}")));
ParcelClient client = new ParcelClient(wireMock.getHttpBaseUrl(), Duration.ofSeconds(2));
assertEquals("{\"status\":\"IN_TRANSIT\"}", client.track("p5"));
assertEquals("{\"status\":\"DELIVERED\"}", client.track("p5"));
assertEquals("{\"status\":\"DELIVERED\"}", client.track("p5"));
}
Every scenario starts in Scenario.STARTED, and resetAllScenarios() moves all of them back. The same pattern tests a retry, where the first stub returns 503 and switches to a state whose stub returns 200, as described in the WireMock stateful behaviour docs.
10. Using WireMock with Spring Boot Tests
In a Spring Boot application, the base URL of the remote API comes from a property. The test starts WireMock first and registers its URL with @DynamicPropertySource, so the application context reads the mock URL when it starts.
class ParcelServiceIntegrationTest {
@RegisterExtension
static WireMockExtension parcelApi = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@DynamicPropertySource
static void parcelApiUrl(DynamicPropertyRegistry registry) {
registry.add("parcels.api.base-url", parcelApi::baseUrl);
}
}
The class also carries @SpringBootTest, and a @ConfigurationProperties class or @Value field reads parcels.api.base-url. The static extension field matters here, because Spring reads the property before any test method runs. More about test slices and contexts is in Spring Boot testing.
11. WireMock vs MockWebServer vs Mockito
Several tools replace a remote API in tests, and they differ in what they run and what they prove.
| Tool | What runs | Best fit |
|---|---|---|
| WireMock | A real HTTP server with a matching DSL, JSON stub files, recording and a standalone mode | Many endpoints, shared stubs, tests in any language against one mock |
| MockWebServer | A real HTTP server with a response queue or a dispatcher | One client class, quick tests with little setup |
| Mockito | No HTTP, the client object is replaced | Unit tests of the code that uses the client |
| Testcontainers | The real dependency in a Docker container | Databases and brokers, or a real copy of the other service |
WireMock costs more setup than MockWebServer, and in return its stubs can be saved as JSON mappings, recorded from a real API and shared with other teams. For a single client class with two or three calls, MockWebServer is often enough.
12. WireMock FAQs
Ports, resets and unmatched requests start to matter once WireMock runs in a real build, and most questions come from that stage.
12.1. Does WireMock reset stubs between tests?
Yes, with the JUnit extension. Both @WireMockTest and WireMockExtension remove stubs and recorded requests before each test method. With a manual WireMockServer, call resetAll() in @AfterEach.
12.2. Which port does WireMock use?
A new WireMockServer() listens on port 8080, while @WireMockTest and options().dynamicPort() pick a free random port. Read it with WireMockRuntimeInfo.getHttpPort() or wireMock.port(), and pass the base URL to the client.
12.3. Why does WireMock return 404 for a request that should match?
Because one condition of the stub does not match, such as a query parameter with urlEqualTo, a header value or one body field. The 404 response body shows the closest stub and marks the field that differs. Switching to urlPathEqualTo fixes the most common case.
12.4. Can WireMock run outside of Java tests?
Yes. The standalone JAR runs as a process with java -jar wiremock-standalone-3.13.2.jar –port 8089, and there is an official Docker image. Stubs are added as JSON files in a mappings folder or through the admin API under /__admin.
12.5. What is the difference between wiremock and wiremock-standalone?
The wiremock artifact declares Jetty, Jackson and Guava as normal dependencies, while wiremock-standalone shades them into one JAR. Use the standalone artifact when those libraries clash with the versions in the application.
13. Conclusion
WireMock plays a remote REST API inside our tests. With @WireMockTest or WireMockExtension, each test class gets a server on a random port and clean stubs for every test. Stubs match on the URL, query parameters, headers and the body, and the near-miss report on a 404 shows why a request did not match.
Beyond the happy path, delays and faults check our timeouts and error handling, verify() checks the requests our code sent, and scenarios model flows where the answer changes over time. For Spring Boot, a static extension and @DynamicPropertySource connect the application to the mock.
14. References
- WireMock, JUnit Jupiter extension
- WireMock, Request Matching
- WireMock, Simulating Faults
- WireMock, Verifying
- WireMock, Stateful Behaviour
Happy Learning !!
Thanks too much! Helped to resolve issues with wiremock