WireMock Tutorial: Mock REST APIs in Java with WireMock 3

WireMock 3 tutorial for Java. Stub REST APIs in JUnit 5 tests, match requests, simulate delays and faults, verify calls and use stateful scenarios.

WireMock request matching flow from HTTP client to stub mappings, response and request journal

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.

WireMock request matching flow from HTTP client to stub mappings, response and request journal
Each request either matches a stub and gets its response, or gets a 404 with a near-miss report, and every request lands in the journal

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 matcherMatchesExample
urlEqualToPath and query string, character by characterurlEqualTo(“/parcels?carrier=dhl”)
urlPathEqualToPath only, any query stringurlPathEqualTo(“/parcels”)
urlPathTemplatePath with named parametersurlPathTemplate(“/parcels/{id}”)
urlMatching / urlPathMatchingA regular expressionurlPathMatching(“/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.

SettingWhat 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_PEERCloses the connection with a TCP reset
Fault.EMPTY_RESPONSECloses the connection without sending anything
Fault.MALFORMED_RESPONSE_CHUNKSends a 200 status followed by invalid chunked data
Fault.RANDOM_DATA_THEN_CLOSESends 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.

ToolWhat runsBest fit
WireMockA real HTTP server with a matching DSL, JSON stub files, recording and a standalone modeMany endpoints, shared stubs, tests in any language against one mock
MockWebServerA real HTTP server with a response queue or a dispatcherOne client class, quick tests with little setup
MockitoNo HTTP, the client object is replacedUnit tests of the code that uses the client
TestcontainersThe real dependency in a Docker containerDatabases 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

Happy Learning !!

Source Code on Github

Leave a Comment

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.