Skip to content
Spring Boot sb testing 3 min read

WireMock & External APIs

Code that calls a third-party HTTP API is hard to test: the real service is slow, rate-limited, costs money, or simply isn’t reachable from CI. WireMock solves this by running a real HTTP server that returns the responses you program — so your WebClient or RestClient makes genuine network calls, but to a server you control. That makes it ideal for asserting how your code behaves on success, errors, timeouts, and malformed responses.

Unlike mocking the HTTP client with Mockito, WireMock exercises the real serialization, connection, and error-handling paths — closer to production while still deterministic.

Adding the dependency

WireMock publishes a JUnit 5 module. For Spring Boot 3 use the Jakarta-compatible standalone artifact:

<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock-standalone</artifactId>
    <version>3.9.1</version>
    <scope>test</scope>
</dependency>

The class under test calls an external exchange-rate API:

@Service
public class RateClient {

    private final RestClient restClient;

    public RateClient(RestClient.Builder builder,
                      @Value("${rates.base-url}") String baseUrl) {
        this.restClient = builder.baseUrl(baseUrl).build();
    }

    public BigDecimal usdToEur() {
        Rate rate = restClient.get()
                .uri("/v1/usd/eur")
                .retrieve()
                .body(Rate.class);
        return rate.value();
    }

    public record Rate(BigDecimal value) {}
}

Stubbing responses with @WireMockTest

@WireMockTest starts a WireMock server on a random port for the test and injects details via WireMockRuntimeInfo. Point the application’s base URL at that port:

@SpringBootTest
@WireMockTest(httpPort = 0)
class RateClientTest {

    @Autowired RateClient rateClient;

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry registry) {
        registry.add("rates.base-url",
                () -> "http://localhost:" + RateClientTest.port);
    }

    static int port;

    @BeforeAll
    static void capturePort(WireMockRuntimeInfo info) {
        port = info.getHttpPort();
    }

    @Test
    void returnsTheRate() {
        stubFor(get("/v1/usd/eur")
                .willReturn(okJson("""
                        { "value": 0.92 }
                        """)));

        assertThat(rateClient.usdToEur())
                .isEqualByComparingTo("0.92");
    }
}

stubFor(...), get(...), and okJson(...) are static imports from com.github.tomakehurst.wiremock.client.WireMock.

Note: Because the random port is needed before the Spring context starts, the example above captures it in @BeforeAll. The cleaner alternative is the WireMockExtension below, where you control the lifecycle explicitly.

Using WireMockExtension for full control

WireMockExtension is a @RegisterExtension field — useful when you want a fixed config, options like response templating, or to read the port early for @DynamicPropertySource:

@SpringBootTest
class RateClientExtensionTest {

    @RegisterExtension
    static WireMockExtension wm = WireMockExtension.newInstance()
            .options(wireMockConfig().dynamicPort())
            .build();

    @Autowired RateClient rateClient;

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry registry) {
        registry.add("rates.base-url", wm::baseUrl);
    }

    @Test
    void parsesSuccessResponse() {
        wm.stubFor(get(urlPathEqualTo("/v1/usd/eur"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody("{\"value\": 0.91}")));

        assertThat(rateClient.usdToEur()).isEqualByComparingTo("0.91");
    }
}

Tip: A static extension instance is shared across all tests in the class and resets stubs between methods. Use a non-static field with @RegisterExtension if you want a fresh server per test method.

Verifying outgoing requests

Beyond returning data, WireMock records every request, so you can assert your client sent the right path, headers, query params, and body:

@Test
void sendsApiKeyHeader() {
    wm.stubFor(get(urlPathEqualTo("/v1/usd/eur"))
            .willReturn(okJson("{\"value\": 0.92}")));

    rateClient.usdToEur();

    wm.verify(getRequestedFor(urlPathEqualTo("/v1/usd/eur"))
            .withHeader("Accept", containing("application/json")));
}

Matching requests precisely

Stubs can match on more than the path — query params, headers, and JSON body:

MatcherPurpose
urlPathEqualTo("/x")Exact path, ignoring query string
withQueryParam("base", equalTo("usd"))Query parameter value
withHeader("Authorization", containing("Bearer"))Header substring
withRequestBody(equalToJson("{...}"))Structural JSON body match
priority(n)Disambiguate overlapping stubs

Simulating failures and latency

This is where WireMock pays off for resilience testing — drive your retry, timeout, and circuit-breaker logic by injecting faults:

// Server error → assert your error handling / retry kicks in
wm.stubFor(get(urlPathEqualTo("/v1/usd/eur"))
        .willReturn(aResponse().withStatus(503)));

// Slow response → assert your read timeout fires
wm.stubFor(get(urlPathEqualTo("/v1/usd/eur"))
        .willReturn(okJson("{\"value\": 0.92}")
                .withFixedDelay(5000)));

// Protocol-level fault → assert the client surfaces a connection error
wm.stubFor(get(urlPathEqualTo("/v1/usd/eur"))
        .willReturn(aResponse()
                .withFault(Fault.CONNECTION_RESET_BY_PEER)));

A timeout test then asserts the right exception propagates:

@Test
void readTimeoutSurfacesException() {
    wm.stubFor(get(urlPathEqualTo("/v1/usd/eur"))
            .willReturn(okJson("{\"value\": 0.92}").withFixedDelay(5000)));

    assertThatThrownBy(() -> rateClient.usdToEur())
            .isInstanceOf(ResourceAccessException.class);
}

Warning: Make sure the client’s read timeout is actually configured (e.g. via ClientHttpRequestFactorySettings on the RestClient.Builder). Without a timeout, withFixedDelay simply makes the test slow instead of triggering the failure path you intended to verify.

WireMock as a container

For integration tests that already use Testcontainers, the official WireMock module starts the server in Docker and keeps test infrastructure uniform:

@Container
static WireMockContainer wiremock =
        new WireMockContainer("wiremock/wiremock:3.9.1");

Point rates.base-url at wiremock.getBaseUrl() via @DynamicPropertySource. The container approach is heavier than the in-process extension, so prefer the extension for unit-level client tests and reserve containers for full-stack scenarios.

Last updated June 13, 2026
Was this helpful?