Overview
This skill covers the JVM Java API for WireMock stub-mapping authoring and request verification (wiremock-quickstart (opens in new window)). WireMock also has standalone JAR + Docker modes for non-JVM consumers - the matching skill for JS / TS is msw-handlers.
When to use
Install
Maven
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>(Per wiremock-quickstart (opens in new window); pin ${wiremock.version} to the team's chosen 3.x release.)
Gradle
testImplementation 'org.wiremock:wiremock:3.x' // pin to the chosen releaseAuthoring stubs
JUnit 4 with @Rule
Per wiremock-quickstart (opens in new window):
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import org.junit.Rule;
public class MyServiceTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(8089);
@Test
public void example() {
stubFor(post("/my/resource")
.withHeader("Content-Type", containing("xml"))
.willReturn(ok()
.withHeader("Content-Type", "text/xml")
.withBody("<response>SUCCESS</response>")));
// ... test the SUT against http://localhost:8089
}
}JUnit 5 with @WireMockTest
For JUnit 5, use @WireMockTest (or WireMockExtension for finer control):
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import org.junit.jupiter.api.Test;
@WireMockTest(httpPort = 8089)
class MyServiceTest {
@Test
void example() {
stubFor(get("/orders/42")
.willReturn(jsonResponse(
"{\"order_id\": 42, \"status\": \"shipped\"}", 200)));
// exercise SUT against http://localhost:8089/orders/42
}
}Pair @WireMockTest with dynamic-port allocation (wireMockConfig().dynamicPort()) for parallel tests, then read the assigned port via WireMockRuntimeInfo.
Stub matching, response shaping, and scenarios
The request-matcher DSL (get / urlPathMatching / withHeader / withQueryParam / matchingJsonPath / withCookie, first-match-wins), the response builders (ok() / okJson() / withFixedDelay / withChunkedDribbleDelay), and stateful scenario stubs are cataloged in references/matchers-and-scenarios.md.
Request verification
After exercising the SUT, assert on requests received:
verify(postRequestedFor(urlEqualTo("/orders"))
.withRequestBody(matchingJsonPath("$.sku", equalTo("SKU-1"))));
verify(exactly(1), getRequestedFor(urlPathEqualTo("/health")));verify() throws on mismatch - fails the test with a clear explanation of expected vs. actual requests.
CI integration
# .github/workflows/integration.yml (excerpt)
- name: Run integration tests
run: mvn -B verify # WireMock starts in-process per @WireMockTest annotation
- name: Upload WireMock logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: wiremock-logs
path: |
target/wiremock-*.log
target/surefire-reports/
retention-days: 14WireMock writes to JUL by default; route to your project's logger to capture stub-match misses (a common cause of "test passed locally, failed on CI" puzzles).
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Hard-coded port 8089 across many test classes | Port collisions in parallel test execution. | Use wireMockConfig().dynamicPort(); read the assigned port from runtime info. |
Stubs that match everything (get(anyUrl())) | Hides bugs - your SUT calls a wrong URL and the test still passes. | Match on specific paths; use verify() to assert exact URLs. |
Skipping verify() after exercising the SUT | The test passes if the SUT skips the call entirely (broken control flow). | Always verify() the expected request was made. |
| Standalone WireMock as a separate process in CI | Race conditions on startup; harder to debug. | Prefer in-process WireMock via @WireMockTest; standalone only when you must mock from outside the JVM. |
| Recording from production | Captures real PII; hard to scrub. | Record from staging only; if from prod, post-process to strip PII. |
Limitations
References
Stub matching, response shaping, and stateful scenarios
View source (opens in a new tab) (opens in new window)Stub matching
The stubFor DSL composes a request-matcher chain:
| Matcher | Purpose |
|---|---|
get("/path") / post("/path") / etc. | HTTP verb + path matcher. |
urlPathMatching("/users/[0-9]+") | Regex on path. |
withHeader("Content-Type", containing("json")) | Header value matcher. |
withQueryParam("status", equalTo("active")) | Query parameter matcher. |
withRequestBody(matchingJsonPath("$.amount", greaterThan(0))) | JSON-path body matcher. |
withCookie("session", equalTo("abc")) | Cookie matcher. |
Stubs are first-match-wins by default; the most specific stub should be registered first.
Response shaping
stubFor(get("/orders/42")
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"order_id\": 42}")
.withFixedDelay(500) // simulate latency
// OR
.withChunkedDribbleDelay(5, 1000) // simulate slow chunked transfer
));Common helper response builders:
| Helper | Effect |
|---|---|
ok() | 200 OK with empty body. |
okJson("...") | 200 + Content-Type: application/json + body. |
notFound(), badRequest(), serverError() | 404 / 400 / 500. |
temporaryRedirect("/new") | 307 + Location. |
Stateful stubs (scenarios)
For workflows that depend on prior state (e.g. "first call returns empty cart, second call returns populated cart"):
stubFor(get("/cart")
.inScenario("Add to cart")
.whenScenarioStateIs(STARTED)
.willReturn(okJson("[]")));
stubFor(post("/cart/add")
.inScenario("Add to cart")
.whenScenarioStateIs(STARTED)
.willSetStateTo("Added")
.willReturn(ok()));
stubFor(get("/cart")
.inScenario("Add to cart")
.whenScenarioStateIs("Added")
.willReturn(okJson("[{\"sku\":\"SKU-1\"}]")));Mountebank - multi-protocol mock servers
View source (opens in a new tab) (opens in new window)Reference detail for wiremock-stubs (opens in new window). WireMock and MSW are HTTP-only; Mountebank covers the multi-protocol long tail. Author imposters (mock servers) by POSTing JSON definitions to the control API on port 2525.
Per mountebank-readme (opens in new window), supported protocols include: HTTP / HTTPS, TCP (text and binary), SMTP, LDAP, gRPC, WebSockets, GraphQL, SNMP, Telnet / SSH, and NETCONF.
Docs-domain note (verified 2026-05-04): the canonical
mbtest.orgdomain was hijacked (redirects to an unrelated site), so this reference cites the GitHub repo bbyars/mountebank (opens in new window);mbtest.devis the project's alternate docs domain. Verify both URLs before linking from authored content.
When to use
If the team is HTTP-only on the JVM, WireMock (the parent skill) is the lighter fit. For Node / browser HTTP-only, use msw-handlers. Mountebank's strength is multi-protocol breadth; pay the operational cost (a separate process, port 2525) only when you need it.
How to use
Install
npm install -g @mbtest/mountebank(Per mountebank-readme (opens in new window).)
For Docker-based CI (preferred for runner cleanliness):
docker run --rm -p 2525:2525 -p 4545:4545 bbyars/mountebank:latest startThe control API listens on port 2525; imposter ports (4545 in the example) are configured per imposter.
Authoring imposters
Mountebank's data model uses these layers:
| Layer | Purpose |
|---|---|
| Imposter | One mock server bound to a port and protocol. |
| Stub | A request matcher attached to an imposter - the response triggered when matched. |
| Predicate | A condition on the incoming request (path, method, header, body, JSON path). |
| Response | The reply Mountebank sends when a stub's predicates match. |
Create an HTTP imposter
POST to the control API:
curl -X POST http://localhost:2525/imposters \
-H 'Content-Type: application/json' \
-d '{
"port": 4545,
"protocol": "http",
"stubs": [{
"predicates": [{
"and": [
{ "equals": { "method": "GET", "path": "/orders/42" } }
]
}],
"responses": [{
"is": {
"statusCode": 200,
"headers": { "Content-Type": "application/json" },
"body": "{\"order_id\": 42, \"status\": \"shipped\"}"
}
}]
}]
}'After this POST, GET http://localhost:4545/orders/42 returns the stubbed response.
Predicate operators
| Operator | Meaning |
|---|---|
equals | Exact match. |
deepEquals | Deep equality on a nested object (e.g. JSON body). |
contains | Substring / partial match. |
startsWith / endsWith | Affix matchers. |
matches | Regex match. |
exists | Whether a field is present. |
not / or / and | Boolean combinators. |
inject | Custom JavaScript predicate. |
Multi-stub responses (cycle through)
If a stub has multiple responses, Mountebank cycles through them in order on subsequent matching requests:
{
"stubs": [{
"predicates": [{ "equals": { "method": "GET", "path": "/poll" } }],
"responses": [
{ "is": { "statusCode": 202 } },
{ "is": { "statusCode": 202 } },
{ "is": { "statusCode": 200, "body": "DONE" } }
]
}]
}Three calls: 202, 202, 200, then it cycles back. Useful for modeling polling endpoints.
Proxying for record-playback
Set up an imposter as a proxy to a real upstream:
{
"port": 4545,
"protocol": "http",
"stubs": [{
"predicates": [{ "matches": { "path": ".*" } }],
"responses": [{
"proxy": {
"to": "https://real-upstream.example.com",
"mode": "proxyOnce"
}
}]
}]
}| Mode | Behavior |
|---|---|
proxyOnce | First request hits upstream; response is stored as a stub; subsequent identical requests replay. |
proxyAlways | Every request hits upstream; every response is stored. |
proxyTransparent | Pass-through; nothing recorded. |
proxyOnce is the canonical record-playback workflow - run tests once against a real upstream to populate the imposter, then run forever offline. Each distinct request hits the upstream and Mountebank stores the response as a stub; on every later run the stored stubs answer and the real API is never called.
Test framework integration
For Node.js test suites, use the mountebank npm package programmatically:
import mb from 'mountebank';
const mbServer = await mb.create({ port: 2525, allowInjection: true });
// POST imposter via fetch / axios / the mb client lib
await fetch('http://localhost:2525/imposters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port: 4545, protocol: 'http', stubs: [...] }),
});
// Run tests against http://localhost:4545
// Tear down
await fetch('http://localhost:2525/imposters/4545', { method: 'DELETE' });
await mbServer.close();CI integration
# .github/workflows/integration.yml
- name: Start Mountebank
run: |
npx -p @mbtest/mountebank mb start &
npx wait-on http://localhost:2525
- name: Seed imposters
run: bash scripts/seed-mountebank.sh
- run: npm test
- name: Stop Mountebank
if: always()
run: pkill -f 'mountebank' || trueFor a more robust pattern, run Mountebank in Docker as a sidecar service rather than a background process - kills + cleanup are cleaner.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Hard-coded imposter ports across many tests | Port collisions under parallel CI execution. | Use dynamic ports; capture them from the control API's response. |
| Predicates with regex that match unintended paths | Test passes because the wrong stub responded. | Anchor regexes (^/$); prefer equals over matches when possible. |
allowInjection: true in production-adjacent envs | JS injection is powerful; allows arbitrary code execution. | Only enable for local / CI; never on a shared mock server. |
| Forgetting to delete imposters between test runs | Stale imposters persist across runs; tests interfere. | DELETE /imposters/<port> in test teardown OR restart Mountebank. |
Recording in proxyAlways mode and committing the captures | Captures may include real PII / tokens. | Use proxyOnce; review captured stubs before committing; scrub PII via JSON Schema or jq pre-commit. |