Setting Up WireMock with Spring Boot: Local API Simulation Guide

Spring Boot integration tests that call real downstream services break in CI, slow local loops, and produce non-deterministic results. The wiremock-spring-boot starter solves this by embedding a full WireMock HTTP server directly into the Spring test context, giving each test suite a controllable, isolated replacement for every external API.

Context: Why the Standalone JAR Alone Is Not Enough Here

The WireMock Standalone Configuration approach — launching a JAR as a background process — works well for polyglot teams and Docker sidecar architectures. For Spring Boot specifically, however, that model introduces a lifecycle gap: the test framework starts before the mock server is ready, and URL binding requires manual coordination.

The wiremock-spring-boot starter closes that gap by tying server startup to ApplicationContext initialisation, exposing the assigned port through @InjectWireMock, and wiring Spring’s @DynamicPropertySource mechanism so your RestTemplate or WebClient always points at the correct address. The result is a zero-configuration, parallel-safe mock server that respects mock lifecycle management principles without any external process.

The diagram below shows how the starter’s components fit together inside a Spring test run.

WireMock Spring Boot Integration Architecture Diagram showing how @EnableWireMock, WireMockServer, DynamicPropertySource, and the Spring ApplicationContext interact during a test run JUnit 5 SpringExtension @EnableWireMock (ApplicationContextInitializer) WireMockServer random port bound @DynamicProperty Source → base URL ApplicationContext RestTemplate wired Test class @InjectWireMock stubFor / verify starts starts port injects URL

Solution

1. Add the dependency

Declare wiremock-spring-boot with test scope. The starter pulls in WireMock Core and the JUnit 5 extension — no additional BOM entry is required for Spring Boot 3.x.

Maven:

<dependency>
  <groupId>org.wiremock.integrations</groupId>
  <artifactId>wiremock-spring-boot</artifactId>
  <version>3.1.0</version>
  <scope>test</scope>
</dependency>

Gradle:

testImplementation("org.wiremock.integrations:wiremock-spring-boot:3.1.0")

2. Annotate the test class and inject the server

@EnableWireMock acts as an ApplicationContextInitializer. It starts the mock server on a random port before the context refreshes, which means @DynamicPropertySource methods can read the port before any beans are constructed.

import com.github.tomakehurst.wiremock.WireMockServer;
import org.wiremock.spring.EnableWireMock;
import org.wiremock.spring.InjectWireMock;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableWireMock
class PaymentServiceIntegrationTest {

    @InjectWireMock
    private WireMockServer wireMock;

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        // wireMock is available here because @EnableWireMock runs first
        registry.add("payment.service.base-url", wireMock::baseUrl);
    }
}

Never hardcode the mock server URL in application-test.yml. The port is random per run, and embedding it breaks parallel CI execution. Always use wireMock::baseUrl as a DynamicPropertySource supplier.

3. Define deterministic stubs in @BeforeEach

Use WireMock’s Java DSL to register stubs before each test. The request interception pattern governs how WireMock matches incoming calls — prefer urlPathEqualTo over urlEqualTo so query parameters do not cause unexpected mismatches.

import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.BeforeEach;

@BeforeEach
void setUpStubs() {
    wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v1/auth/token"))
        .withHeader("Content-Type", WireMock.containing("application/json"))
        .willReturn(WireMock.aResponse()
            .withStatus(200)
            .withHeader("Content-Type", "application/json")
            .withBody("""
                {"access_token": "mock-jwt", "expires_in": 3600}
                """)));

    wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v1/payments"))
        .withRequestBody(WireMock.matchingJsonPath("$.amount"))
        .willReturn(WireMock.aResponse()
            .withStatus(201)
            .withHeader("Content-Type", "application/json")
            .withBody("""
                {"paymentId": "mock-payment-001", "status": "ACCEPTED"}
                """)));
}

4. Simulate stateful sequences with scenarios

The WireMock scenario state machine is available in the embedded mode exactly as it is in the standalone server. Use it to simulate polling flows, OAuth token refresh, or any sequence that changes behaviour on repeated calls.

import com.github.tomakehurst.wiremock.stubbing.Scenario;

@Test
void jobPollingEventuallyCompletes() {
    wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo("/api/v1/jobs/42"))
        .inScenario("Job Processing")
        .whenScenarioStateIs(Scenario.STARTED)
        .willReturn(WireMock.aResponse()
            .withStatus(202)
            .withBody("{\"status\": \"PENDING\"}"))
        .willSetStateTo("COMPLETED"));

    wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo("/api/v1/jobs/42"))
        .inScenario("Job Processing")
        .whenScenarioStateIs("COMPLETED")
        .willReturn(WireMock.aResponse()
            .withStatus(200)
            .withBody("{\"status\": \"DONE\", \"result\": \"processed\"}")));

    // exercise the polling client under test — first call returns 202, second returns 200
    jobClient.waitForCompletion("42");

    wireMock.verify(2, WireMock.getRequestedFor(
        WireMock.urlPathEqualTo("/api/v1/jobs/42")));
}

5. Enable response templating for dynamic payloads

Response templating uses Handlebars to interpolate request data into response bodies. Enable it per-stub with .withTransformers("response-template"), or pass --global-response-templating via the args attribute on @EnableWireMock:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableWireMock(args = "--global-response-templating")
class OrderServiceIntegrationTest {

    @InjectWireMock
    private WireMockServer wireMock;

    @BeforeEach
    void setUpStubs() {
        wireMock.stubFor(WireMock.get(WireMock.urlPathMatching("/api/v1/orders/[0-9]+"))
            .willReturn(WireMock.aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                      "orderId": "{{request.pathSegments.[3]}}",
                      "status": "shipped",
                      "requestedAt": "{{now format='yyyy-MM-dd'}}"
                    }
                    """)));
    }
}

6. Inject error responses to test resilience

Inject fault conditions to verify client-side response shaping techniques such as retry logic, exponential backoff, and circuit breaker thresholds:

@Test
void clientRetriesOn503() {
    wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v1/payments"))
        .inScenario("Retry")
        .whenScenarioStateIs(Scenario.STARTED)
        .willReturn(WireMock.aResponse().withStatus(503))
        .willSetStateTo("RECOVERED"));

    wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v1/payments"))
        .inScenario("Retry")
        .whenScenarioStateIs("RECOVERED")
        .willReturn(WireMock.aResponse()
            .withStatus(201)
            .withBody("{\"paymentId\": \"mock-payment-002\"}")));

    PaymentResponse response = paymentClient.submit(buildPayload());

    assertThat(response.getPaymentId()).isEqualTo("mock-payment-002");
    wireMock.verify(2, WireMock.postRequestedFor(
        WireMock.urlPathEqualTo("/api/v1/payments")));
}

Also test 429 Too Many Requests with a Retry-After header, and FaultType.CONNECTION_RESET_BY_PEER for socket-level failures.

Verification

After running the test suite, confirm WireMock is doing what you expect with a single assertion block:

// Confirm the stub was matched the expected number of times
wireMock.verify(1, WireMock.postRequestedFor(
    WireMock.urlPathEqualTo("/api/v1/auth/token"))
    .withHeader("Content-Type", WireMock.containing("application/json")));

// Confirm no unexpected requests reached the mock server
List<LoggedRequest> unmatched = wireMock.findUnmatchedRequests().getRequests();
assertThat(unmatched).isEmpty();

Run the test with verbose logging enabled — add --verbose to the args array on @EnableWireMock — and examine the console output for Request was not matched lines. Each mismatch report lists the actual request fields alongside the expected matcher values, making diagnosis straightforward without a debugger.

Gotchas and Edge Cases

  • @DirtiesContext restarts the server on every test class. The wiremock-spring-boot starter binds the server to the Spring ApplicationContext lifecycle. If @DirtiesContext is present, each test class gets a fresh context and a fresh mock server on a new random port. Remove @DirtiesContext wherever the test does not actually mutate shared Spring beans — this is the most common cause of slow integration test suites.

  • Classpath mapping files are loaded once at startup. If you use @EnableWireMock(mappings = "classpath:/mappings") alongside programmatic stubFor() calls, the file-based stubs are registered first at startup and the programmatic ones layer on top. A programmatic stub with a higher priority (or added later) wins on a match. Reset all stubs between tests with wireMock.resetAll() in an @AfterEach if you mix the two approaches.

  • WireMock.verify() counts are cumulative within a test context. If the context is cached across test classes and you do not call wireMock.resetAll() or at least wireMock.resetRequests(), call counts from a previous test class add to the totals in the current one. Scope your verify() assertions to a single test, or reset the request journal in @BeforeEach.


FAQ

Does @EnableWireMock conflict with @SpringBootTest context caching?

No. The starter registers its server lifecycle through Spring’s ApplicationContextInitializer interface, which runs before bean creation. Contexts that share the same @EnableWireMock configuration (same args, mappings, and files) are reused across test classes. Only @DirtiesContext forces a context reload and server restart — avoid it unless the test genuinely mutates global state.

Can I use file-based JSON mappings instead of the Java DSL?

Yes. Set @EnableWireMock(mappings = "classpath:/mappings", files = "classpath:/__files") and place stub JSON files under src/test/resources/mappings/. WireMock loads them at server startup. You can combine file-based stubs with programmatic stubFor() calls; programmatic stubs take priority when priorities are equal and were added after startup.

How do I enable Handlebars response templating in the embedded mode?

Pass --global-response-templating via @EnableWireMock(args = "--global-response-templating"). Without this flag, {{request.*}} Handlebars expressions in your withBody() strings are returned as literal text instead of interpolated values. Alternatively, add .withTransformers("response-template") to individual stubs to enable templating selectively.


← Back to WireMock Standalone Configuration