Mock External APIs with Mountebank for Integration Tests
Integration tests become difficult when an application depends on payment gateways, identity providers, shipping platforms, or other services outside your control. Real endpoints can be slow, expensive, rate-limited, unavailable during development, or filled with data that is hard to reproduce. A reliable test suite needs a controllable substitute that behaves like the external dependency without contacting it.
Mountebank is an open-source service virtualisation tool designed for this purpose. It runs lightweight HTTP, HTTPS, TCP, and other protocol imposters that return predefined responses to requests. Tests can then exercise the application’s real networking code while the external system remains isolated.
For teams building Java or Groovy services, Mountebank fits naturally into local development, CI pipelines, Docker-based environments, and continuous delivery workflows. It is especially useful for checking retry behaviour, error handling, authentication flows, asynchronous callbacks, and contract assumptions before deploying to an Australian cloud region or production environment.
| Approach | Strengths | Limitations | Suitable use |
|---|---|---|---|
| Live external API | Highest environmental realism | Slow, unstable, costly, difficult to control | Small smoke tests |
| Hand-written mock inside the application | Quick to add | Can bypass HTTP and client configuration | Unit tests |
| WireMock or similar HTTP stub server | Rich HTTP features and broad adoption | Requires another tool and configuration style | HTTP integration tests |
| Mountebank | Protocol support, request verification, fault simulation, easy process isolation | Requires learning imposters and predicates | Service virtualisation and integration testing |
| Recorded API responses | Reflects real payloads | May become stale or expose sensitive data | Regression fixtures |
Why External Service Simulation Matters
A unit test can mock a Java interface and verify that a method was called. That does not prove the HTTP client uses the correct URL, headers, serialisation rules, timeouts, or status-code handling. An integration test that sends a request through the application’s real client provides stronger evidence, while Mountebank supplies the predictable remote service.
This distinction matters for systems integrating with Australian banks, Medicare-related providers, parcel carriers, and domestic payment platforms. A sandbox may have business-hour restrictions, limited test cards, or occasional maintenance windows. Mountebank lets developers reproduce important responses at any time, whether the team is working in Sydney, Melbourne, Perth, or remotely across different time zones.
The goal is not to imitate every detail of an external vendor. It is to model the behaviours that affect your application. A small set of successful, invalid, delayed, empty, and unavailable responses usually provides more testing value than a large collection of copied production payloads.
Mountebank calls each simulated service an imposter. An imposter listens on a port and contains stubs. Each stub has predicates, which determine whether an incoming request matches, and responses, which define what Mountebank returns. This structure keeps test scenarios explicit and version-controlled.
Creating a Basic Mountebank Imposter
Install Mountebank with npm or run it as a container. A simple Docker command exposes the default management port and allows the application to reach the simulator through a predictable hostname:
docker run --rm --name mb \
-p 2525:2525 \
-p 4545:4545 \
bbyars/mountebank:latest \
start --allowInjection
The management API commonly listens on port 2525, while an individual HTTP imposter can listen on port 4545. A configuration file can describe the imposter in JSON or JavaScript. The following example responds to a specific account lookup:
{
"port": 4545,
"protocol": "http",
"stubs": [
{
"predicates": [
{
"equals": {
"method": "GET",
"path": "/v1/accounts/123"
}
}
],
"responses": [
{
"is": {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"id\":\"123\",\"status\":\"active\",\"balance\":125.50}"
}
}
]
}
]
}
Create the imposter through the management endpoint:
curl -X POST http://localhost:2525/imposters \
-H 'Content-Type: application/json' \
--data-binary @account-imposter.json
Your application should point its external-service base URL to http://localhost:4545, or to the container hostname when both services run in Docker Compose. Keep that endpoint configurable through an environment variable rather than embedding a test address in production configuration.
For Java and Groovy projects, REST Assured, Spock, or JUnit can drive the application’s API while Mountebank handles the downstream call. The application still performs serialisation, connection establishment, authentication-header creation, and response mapping, which makes the test closer to the deployed system.
Matching Requests Precisely
Predicates control which stub handles a request. You can match the HTTP method, path, query parameters, headers, or body. Exact matching is useful when a test is checking a contract, while regular expressions or partial matching are better when values such as timestamps and correlation IDs change on every request.
A payment request might include a generated idempotency key, so matching the entire body could make a test unnecessarily brittle. Instead, match the path and method, then use a body predicate that checks the meaningful fields. The correct level of strictness depends on what the test is intended to prove: client formatting, business behaviour, or resilience.
Multiple stubs can represent different cases. A successful response may match an account ID ending in 123, while another predicate returns 404 for an unknown account. A third can return 422 for invalid data. Put specific predicates before broad ones, and make every scenario identifiable from the test name or fixture so failures are easy to diagnose.
Mountebank can also record requests received by an imposter. Verification helps establish that the application made the expected call, with the expected method, endpoint, headers, and number of attempts. This is valuable for detecting accidental duplicate payments or missing authentication, although request verification should support the test’s purpose rather than turn every harmless header change into a failure.
When using asynchronous workflows, configure one response for the initial request and use a separate imposter for a webhook or callback endpoint. The test can then trigger the downstream event and verify that the application updates its state correctly. This approach is useful for order dispatch, payment settlement, and message-driven processing.
Simulating Failure and Unusual Responses
A successful 200 OK response exercises the least interesting path. External API integration tests should cover timeouts, connection resets, malformed JSON, 401 and 403 responses, rate limiting, server errors, and slow responses. Mountebank can return custom status codes and headers, and its response behaviours can introduce delays or close connections.
For example, a rate-limit stub might return:
{
"is": {
"statusCode": 429,
"headers": {
"Content-Type": "application/json",
"Retry-After": "2"
},
"body": "{\"error\":\"too_many_requests\"}"
}
}
This allows the test to verify whether the client honours Retry-After, applies a bounded retry policy, and eventually reports a useful error. A delayed response can check socket and request timeouts, while a sequence of responses can model a temporary 503 followed by success.
Failure simulation should remain deterministic. Avoid random errors in the main integration suite because they create intermittent failures that conceal real regressions. Use named scenarios such as payment-provider-timeout or identity-service-invalid-token, and activate the required imposter configuration at test setup.
Tests should also assert operational behaviour. If a dependency is unavailable, does the application return a clear 503, place work on a queue, or use cached data? Does it log a correlation ID without recording card details or personal information? These checks connect functional correctness with production safety.
Australian services may have strict privacy and data-residency expectations, particularly when customer identity or health information is involved. Synthetic Mountebank payloads prevent real customer data from entering a developer laptop or a CI runner, including pipelines hosted outside Australia. Use fake names, account numbers, and addresses in fixtures.
Running Mountebank in Continuous Integration
Treat imposter definitions as test code. Store them beside the integration tests, review changes through pull requests, and give each fixture a clear purpose. A Docker Compose setup can start the application, Mountebank, a database, and any message broker on an isolated network. The test environment then resembles the service topology without requiring access to vendor sandboxes.
A typical pipeline has four stages: start the simulator, load imposters, run application tests, and collect logs or request recordings when a test fails. Remove imposters after the job so one scenario cannot leak into another. Parallel jobs should use separate ports or separate containers to avoid shared state.
Mountebank is a simulator, not a replacement for all vendor testing. Keep a small number of scheduled contract or smoke tests against the provider’s sandbox, subject to its terms and rate limits. Those tests can reveal authentication changes or schema differences that local stubs cannot detect. The larger suite should continue using Mountebank for speed and repeatability.
Configuration should distinguish local, CI, and sandbox endpoints. For example, a Spring Boot property or environment variable can select the base URL, while secrets remain in the CI secret store. Do not place API keys in imposter files, and do not commit recorded responses containing tokens, addresses, or other sensitive information.
Keeping Simulated Contracts Trustworthy
An imposter becomes harmful when it describes an API that the application no longer receives. Review response fixtures when the external provider changes its schema, and record the assumptions that matter: required fields, nullable values, status codes, pagination, and error formats. A short contract document beside the fixture makes the simulation easier to maintain.
Use Mountebank at the integration boundary, then keep business rules covered by fast unit tests. This separation prevents a large suite of HTTP scenarios from becoming the only safety net. Integration tests should prove wiring and dependency behaviour; unit tests should explore calculations, mapping rules, and domain decisions in greater depth.
For a practical starting point, choose one important downstream operation, create success and failure imposters, point the test environment at Mountebank, and run the integration test in CI. The concrete next step is to add a payment-provider-timeout fixture and verify that the application returns its expected 503 response without contacting the real provider.