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.
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
-
@DirtiesContextrestarts the server on every test class. Thewiremock-spring-bootstarter binds the server to the SpringApplicationContextlifecycle. If@DirtiesContextis present, each test class gets a fresh context and a fresh mock server on a new random port. Remove@DirtiesContextwherever 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 programmaticstubFor()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 withwireMock.resetAll()in an@AfterEachif 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 callwireMock.resetAll()or at leastwireMock.resetRequests(), call counts from a previous test class add to the totals in the current one. Scope yourverify()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.
Related
- Running WireMock in Docker Compose — isolate WireMock as a Docker sidecar instead of embedding it in the test JVM
- Writing Custom MSW Response Resolvers — comparable approach for browser-layer mocking with MSW
- Mock Lifecycle Management — general principles for startup, teardown, and state reset across all mock server types
← Back to WireMock Standalone Configuration