Use whenever Java/JVM tests call a slow, flaky, rate-limited or paid HTTP API: stub it with WireMock so tests run offline, including faults such as timeouts and 500 responses, request verification and parallel-safe ports. Mountebank for TCP, SMTP, LDAP and gRPC is in its references.
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Reference detail for wiremock-stubs. 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, 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;mbtest.devis the project's alternate docs domain. Verify both URLs before linking from authored content.
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.
mb start or the Docker image); the control API listens on port 2525./imposters with a port, a protocol, and one or more stubs.predicates (path / method / header / body matchers) and responses (the reply to send).GET http://localhost:2525/imposters/<port> and assert HTTP 200 with your stubs listed before pointing tests at it. If it 404s or the stub is missing, the POST body was malformed - fix the JSON and re-POST.proxyOnce proxy response to capture real traffic, then replay offline.DELETE /imposters/<port> in teardown (or restart Mountebank) so stale stubs don't leak between runs.npm install -g @mbtest/mountebank(Per mountebank-readme.)
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.
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. |
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.
| 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. |
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.
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.
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();# .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-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. |
stubFor.mbtest.dev.msw-handlers - HTTP-only alternative for browser + Node.