StackPractices
intermediate By Mathias Paulenko

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.

Topics: testing

Overview

sequenceDiagram diagram: participant Test

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 means you control status codes, headers, body, delays, and even stateful behavior — all without leaving the test JVM.

I’ve reached for WireMock on three or four projects — mostly payment gateway integrations where hitting the real API in tests wasn’t an option. It’s my go-to when the code under test makes real HTTP calls and I need to control exactly what comes back. For broader integration testing strategies, see our integration testing strategies recipe. If you’re testing containerized services like databases or message brokers, check our Testcontainers recipe instead.

When to Use

  • Testing code that talks to an external REST API — payment gateways, SMS providers, email services, that kind of thing — without depending on the real service.
  • Simulating failures — timeouts, 500s, slow responses, rate limits — so your retry and fallback logic actually gets exercised.
  • 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 MockMvc or WebTestClient.
  • Pure business-logic unit tests. Just mock the interface with Mockito — you don’t need a real HTTP server for those.
  • Load testing. WireMock adds overhead — use a real test environment for load.
  • Database interactions. Reach for 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();

Explanation

WireMock works by embedding a Jetty HTTP server inside the test JVM. When you call stubFor(), WireMock registers a mapping in its internal store. When the code under test makes an HTTP request to WireMock’s URL, the server matches the request against registered stubs using URL path, method, headers, and body patterns. The first matching stub wins — registration order matters when stubs overlap.

The key difference between WireMock and Mockito is scope. Mockito mocks Java interfaces — it intercepts method calls at the language level. WireMock mocks HTTP — it stands up a real server that speaks the protocol. Use Mockito when your code calls an interface you control. Use WireMock when your code talks to an external service over HTTP — HttpClient, RestTemplate, OkHttp, doesn’t matter which client.

I’ve seen teams try to mock HttpClient with Mockito and end up with brittle tests that pass but don’t catch real serialization or header issues. WireMock catches those because the HTTP call is real — only the server on the other end is fake.

WireMock vs Mockito vs MockWebServer

FeatureWireMockMockitoMockWebServer
ScopeHTTP-level stubbingInterface-level mockingHTTP-level stubbing
HTTP serverReal Jetty serverNo serverReal OkHttp server
Response templatingHandlebarsN/AN/A
Stateful stubsScenariosN/AN/A
Delay simulationBuilt-inN/AManual
Request verificationURL, body, headersMethod argsRecorded requests
Standalone/DockerYesNoNo
LanguageJava, Kotlin, any JVMJava, KotlinJava, Kotlin
Best forExternal API stubbingUnit testing interfacesSimple HTTP mocking

I prefer WireMock over MockWebServer for anything beyond the simplest stubs. MockWebServer is fine for a quick “return 200 for this URL” test, but WireMock’s scenarios, templating, and recording mode save real time when the API you’re mocking has complex behavior.

Recording mode

WireMock can record real API calls and generate stubs from them. I use this when I’m integrating with an API for the first time and want to capture real responses:

wireMock.startRecording("https://real-api.example.com");
// ... run code that calls the real API through WireMock ...
List<StubMapping> recordedStubs = wireMock.stopRecording().getStubMappings();

Recording mode proxies requests to the real API, captures the responses, and creates stub mappings you can save and replay later. That’s the quickest way I’ve found to bootstrap a test suite for a gnarly API.

WireMock in Docker

For tests that need a shared WireMock instance across two or more services or languages, run it as a Docker container:

docker run -d --name wiremock -p 8080:8080 \
  wiremock/wiremock:3.9.1 --verbose

Mount stubs and files via volumes:

docker run -d -p 8080:8080 \
  -v ./stubs:/home/wiremock/mappings \
  -v ./responses:/home/wiremock/__files \
  wiremock/wiremock:3.9.1

This is useful for contract testing across microservices — each team can share the same WireMock Docker image with pre-loaded stubs.

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 @ResetWireMock annotation.
  • 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 — when a stub won’t match, turn on verbose output with .notifier(new ConsoleNotifier(true)) and WireMock’ll tell you exactly what it tried.

See Also

Frequently Asked Questions

What's the best way to 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 forwards any request that doesn't match a stub to a real server:

wireMock.stubFor(WireMock.any(WireMock.anyUrl())
    .willReturn(WireMock.aResponse().proxiedFrom("https://real-api.example.com")));
Why would 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))