Article
In modern microservices architectures, Java applications typically depend on numerous external services, APIs, and protocols. Testing these integrations can be challenging when dependencies are unavailable, unstable, or expensive to call. Mountebank is a powerful, multi-protocol test double that enables comprehensive service virtualization for testing Java applications.
What is Mountebank?
Mountebank is an open-source tool that provides cross-platform, multi-protocol test doubles over the wire. Unlike traditional mocking frameworks that work within the JVM, Mountebank runs as a separate process and can mock HTTP, HTTPS, TCP, SMTP, and other protocols.
Key Advantages for Java Teams:
- Multi-Protocol Support: HTTP, HTTPS, TCP, SMTP, and more
- Language Agnostic: Mocks work across any language that can make HTTP calls
- Stateful Behavior: Create complex, stateful mock behaviors
- Predicate Matching: Flexible request matching with predicates
- Programmable: Control mocks via REST API
- Easy CI/CD Integration: Simple to include in testing pipelines
Installation and Setup
1. Install Mountebank
Using npm (Recommended):
# Install globally npm install -g mountebank # Or as development dependency npm install --save-dev mountebank # Verify installation mb --version
Using Docker:
# Run Mountebank in Docker docker run -p 2525:2525 -p 4545:4545 bbyars/mountebank:2.8.1 # Or with docker-compose version: '3.8' services: mountebank: image: bbyars/mountebank:2.8.1 ports: - "2525:2525" # Management port - "4545:4545" # Imposter port - "5555:5555" # Additional imposter port
2. Start Mountebank
# Start with default settings mb --port 2525 --allowInjection # Start with configuration file mb --configfile imposters.ejs # Start in the background mb start --port 2525
Java Client for Mountebank
Maven Dependencies
<dependencies> <!-- HTTP Client for interacting with Mountebank --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <!-- AssertJ for fluent assertions --> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.2</version> <scope>test</scope> </dependency> </dependencies>
Mountebank Client Utility
package com.example.mountebank;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.io.IOException;
import java.util.Map;
public class MountebankClient {
private final String mountebankUrl;
private final CloseableHttpClient httpClient;
private final ObjectMapper objectMapper;
public MountebankClient(String mountebankUrl) {
this.mountebankUrl = mountebankUrl;
this.httpClient = HttpClients.createDefault();
this.objectMapper = new ObjectMapper();
}
public MountebankClient() {
this("http://localhost:2525");
}
/**
* Create an imposter on Mountebank
*/
public void createImposter(Map<String, Object> imposterConfig) throws IOException {
HttpPost request = new HttpPost(mountebankUrl + "/imposters");
String jsonConfig = objectMapper.writeValueAsString(imposterConfig);
request.setEntity(new StringEntity(jsonConfig));
request.setHeader("Content-Type", "application/json");
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getCode();
if (statusCode != 201 && statusCode != 400) {
String responseBody = EntityUtils.toString(response.getEntity());
throw new IOException("Failed to create imposter: " + responseBody);
}
}
}
/**
* Delete an imposter
*/
public void deleteImposter(int port) throws IOException {
HttpDelete request = new HttpDelete(mountebankUrl + "/imposters/" + port);
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (response.getCode() != 200) {
throw new IOException("Failed to delete imposter on port " + port);
}
}
}
/**
* Delete all imposters
*/
public void deleteAllImposters() throws IOException {
HttpDelete request = new HttpDelete(mountebankUrl + "/imposters");
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (response.getCode() != 200) {
throw new IOException("Failed to delete all imposters");
}
}
}
/**
* Get imposter details
*/
public String getImposter(int port) throws IOException {
HttpGet request = new HttpGet(mountebankUrl + "/imposters/" + port);
try (CloseableHttpResponse response = httpClient.execute(request)) {
return EntityUtils.toString(response.getEntity());
}
}
/**
* Check if Mountebank is running
*/
public boolean isRunning() {
try {
HttpGet request = new HttpGet(mountebankUrl);
try (CloseableHttpResponse response = httpClient.execute(request)) {
return response.getCode() == 200;
}
} catch (Exception e) {
return false;
}
}
public void close() throws IOException {
httpClient.close();
}
}
HTTP Protocol Mocks
1. Simple HTTP Mock
package com.example.mountebank;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class HttpMockExample {
private final MountebankClient mbClient;
private final ObjectMapper objectMapper;
public HttpMockExample() {
this.mbClient = new MountebankClient();
this.objectMapper = new ObjectMapper();
}
/**
* Create a simple HTTP mock that returns fixed responses
*/
public void createSimpleHttpMock(int port) throws Exception {
Map<String, Object> imposter = new HashMap<>();
imposter.put("port", port);
imposter.put("protocol", "http");
imposter.put("name", "Simple HTTP Mock");
// Define stubs (behaviors)
Map<String, Object>[] stubs = new Map[2];
// First stub - GET /users
Map<String, Object> getUsersStub = new HashMap<>();
Map<String, Object> getUsersPredicate = Map.of(
"equals", Map.of(
"method", "GET",
"path", "/users"
)
);
Map<String, Object> getUsersResponse = Map.of(
"is", Map.of(
"statusCode", 200,
"headers", Map.of("Content-Type", "application/json"),
"body", """
[
{"id": 1, "name": "John Doe", "email": "[email protected]"},
{"id": 2, "name": "Jane Smith", "email": "[email protected]"}
]
"""
)
);
getUsersStub.put("predicates", new Map[]{getUsersPredicate});
getUsersStub.put("responses", new Map[]{getUsersResponse});
// Second stub - POST /users
Map<String, Object> postUsersStub = new HashMap<>();
Map<String, Object> postUsersPredicate = Map.of(
"equals", Map.of(
"method", "POST",
"path", "/users"
)
);
Map<String, Object> postUsersResponse = Map.of(
"is", Map.of(
"statusCode", 201,
"headers", Map.of(
"Content-Type", "application/json",
"Location", "/users/123"
),
"body", """
{"id": 123, "status": "created"}
"""
)
);
postUsersStub.put("predicates", new Map[]{postUsersPredicate});
postUsersStub.put("responses", new Map[]{postUsersResponse});
stubs[0] = getUsersStub;
stubs[1] = postUsersStub;
imposter.put("stubs", stubs);
mbClient.createImposter(imposter);
}
}
2. Advanced HTTP Mock with Dynamic Responses
public class AdvancedHttpMock {
private final MountebankClient mbClient;
public AdvancedHttpMock() {
this.mbClient = new MountebankClient();
}
/**
* Create a mock with dynamic responses and predicates
*/
public void createAdvancedHttpMock(int port) throws Exception {
Map<String, Object> imposter = Map.of(
"port", port,
"protocol", "http",
"name", "Advanced HTTP Mock",
"stubs", new Object[]{
// Mock for user service
createUserServiceStub(),
// Mock for product service
createProductServiceStub(),
// Mock with query parameters
createQueryParamStub(),
// Mock with headers
createHeaderBasedStub(),
// Default response
createDefaultStub()
}
);
mbClient.createImposter(imposter);
}
private Map<String, Object> createUserServiceStub() {
return Map.of(
"predicates", new Object[]{
Map.of("startsWith", Map.of("path", "/api/users"))
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"headers", Map.of("Content-Type", "application/json"),
"body", """
{
"users": [
{"id": 1, "name": "Test User", "active": true},
{"id": 2, "name": "Mock User", "active": false}
],
"total": 2,
"page": 1
}
"""
))
}
);
}
private Map<String, Object> createProductServiceStub() {
return Map.of(
"predicates", new Object[]{
Map.of("equals", Map.of(
"method", "GET",
"path", "/api/products"
))
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"headers", Map.of("Content-Type", "application/json"),
"body", """
{
"products": [
{"id": "P001", "name": "Laptop", "price": 999.99},
{"id": "P002", "name": "Mouse", "price": 29.99}
]
}
"""
))
}
);
}
private Map<String, Object> createQueryParamStub() {
return Map.of(
"predicates", new Object[]{
Map.of("and", new Object[]{
Map.of("equals", Map.of("method", "GET")),
Map.of("equals", Map.of("path", "/api/search")),
Map.of("equals", Map.of("query", Map.of("q", "java")))
})
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"body": """
{
"results": [
{"title": "Java Programming", "type": "book"},
{"title": "Java Coffee", "type": "beverage"}
],
"query": "java"
}
"""
))
}
);
}
private Map<String, Object> createHeaderBasedStub() {
return Map.of(
"predicates", new Object[]{
Map.of("and", new Object[]{
Map.of("equals", Map.of("method", "GET")),
Map.of("equals", Map.of("path", "/api/admin")),
Map.of("equals", Map.of(
"headers", Map.of("Authorization", "Bearer admin-token")
))
})
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"body": """
{
"message": "Admin access granted",
"privileges": ["read", "write", "delete"]
}
"""
))
}
);
}
private Map<String, Object> createDefaultStub() {
return Map.of(
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 404,
"body": """
{
"error": "Endpoint not found",
"message": "The requested resource does not exist"
}
"""
))
}
);
}
}
HTTPS Protocol Mocks
public class HttpsMockExample {
private final MountebankClient mbClient;
public HttpsMockExample() {
this.mbClient = new MountebankClient();
}
/**
* Create HTTPS mock with SSL/TLS
*/
public void createHttpsMock(int port) throws Exception {
Map<String, Object> imposter = Map.of(
"port", port,
"protocol", "https",
"name", "HTTPS API Mock",
"key", "path/to/key.pem", // In real usage, provide actual key paths
"cert", "path/to/cert.pem",
"stubs", new Object[]{
createSecureApiStub()
}
);
mbClient.createImposter(imposter);
}
private Map<String, Object> createSecureApiStub() {
return Map.of(
"predicates", new Object[]{
Map.of("equals", Map.of("path", "/secure/data"))
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"headers", Map.of("Content-Type", "application/json"),
"body": """
{
"secure": true,
"data": "This is secure mock data",
"timestamp": "2023-11-15T10:30:00Z"
}
"""
))
}
);
}
}
TCP Protocol Mocks
public class TcpMockExample {
private final MountebankClient mbClient;
public TcpMockExample() {
this.mbClient = new MountebankClient();
}
/**
* Create TCP mock for binary protocols or custom protocols
*/
public void createTcpMock(int port) throws Exception {
Map<String, Object> imposter = Map.of(
"port", port,
"protocol", "tcp",
"name", "TCP Service Mock",
"mode", "text",
"stubs", new Object[]{
createTcpStub()
}
);
mbClient.createImposter(imposter);
}
private Map<String, Object> createTcpStub() {
return Map.of(
"predicates", new Object[]{
Map.of("contains", Map.of("data", "HELLO"))
},
"responses", new Object[]{
Map.of("is", Map.of("data", "WORLD"))
}
);
}
/**
* Create TCP mock for database protocol simulation
*/
public void createDatabaseTcpMock(int port) throws Exception {
Map<String, Object> imposter = Map.of(
"port", port,
"protocol", "tcp",
"name", "Database TCP Mock",
"mode", "binary",
"stubs", new Object[]{
createDatabaseResponseStub()
}
);
mbClient.createImposter(imposter);
}
private Map<String, Object> createDatabaseResponseStub() {
// Simulate database protocol responses
return Map.of(
"responses", new Object[]{
Map.of("is", Map.of(
"data", "QUERY_RESPONSE_OK"
))
}
);
}
}
Java Test Integration
JUnit 5 Test with Mountebank
package com.example.mountebank.test;
import com.example.mountebank.MountebankClient;
import com.example.mountebank.HttpMockExample;
import org.junit.jupiter.api.*;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MountebankHttpTest {
private MountebankClient mbClient;
private HttpMockExample httpMock;
private CloseableHttpClient testHttpClient;
private static final int MOCK_PORT = 5555;
@BeforeAll
void setUp() throws Exception {
mbClient = new MountebankClient();
httpMock = new HttpMockExample();
testHttpClient = HttpClients.createDefault();
// Ensure Mountebank is running
assertThat(mbClient.isRunning())
.as("Mountebank should be running on localhost:2525")
.isTrue();
}
@BeforeEach
void setUpMock() throws Exception {
// Create fresh mock for each test
httpMock.createSimpleHttpMock(MOCK_PORT);
}
@AfterEach
void tearDownMock() throws Exception {
// Clean up mock after each test
mbClient.deleteImposter(MOCK_PORT);
}
@AfterAll
void tearDown() throws Exception {
testHttpClient.close();
mbClient.close();
}
@Test
void testGetUsersEndpoint() throws Exception {
// When
HttpGet request = new HttpGet("http://localhost:" + MOCK_PORT + "/users");
try (CloseableHttpResponse response = testHttpClient.execute(request)) {
// Then
assertThat(response.getCode()).isEqualTo(200);
assertThat(response.getHeader("Content-Type").getValue())
.contains("application/json");
String responseBody = EntityUtils.toString(response.getEntity());
assertThat(responseBody)
.contains("John Doe")
.contains("Jane Smith")
.contains("\"id\":1")
.contains("\"id\":2");
}
}
@Test
void testPostUsersEndpoint() throws Exception {
// When making POST request to /users
// (In real test, you'd use HttpPost with entity)
// This test verifies the mock was created successfully
String imposterInfo = mbClient.getImposter(MOCK_PORT);
assertThat(imposterInfo)
.contains("\"port\":" + MOCK_PORT)
.contains("\"protocol\":\"http\"");
}
@Test
void testNonExistentEndpoint() throws Exception {
// When
HttpGet request = new HttpGet("http://localhost:" + MOCK_PORT + "/nonexistent");
try (CloseableHttpResponse response = testHttpClient.execute(request)) {
// Then - should get 404 from our default stub
assertThat(response.getCode()).isEqualTo(404);
}
}
}
Spring Boot Test Integration
package com.example.mountebank.test;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
class SpringBootMountebankTest {
private final TestRestTemplate restTemplate = new TestRestTemplate();
private final MountebankClient mbClient = new MountebankClient();
private final HttpMockExample httpMock = new HttpMockExample();
private static final int EXTERNAL_SERVICE_MOCK_PORT = 6666;
private static final int APP_PORT = 8080;
@Test
void testApplicationWithMockedExternalService() throws Exception {
// Given - external service is mocked
httpMock.createSimpleHttpMock(EXTERNAL_SERVICE_MOCK_PORT);
// When - application calls external service
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + APP_PORT + "/api/data",
String.class
);
// Then - application works correctly with mocked service
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
// Clean up
mbClient.deleteImposter(EXTERNAL_SERVICE_MOCK_PORT);
}
}
Stateful Mocks and Behaviors
public class StatefulMockExample {
private final MountebankClient mbClient;
public StatefulMockExample() {
this.mbClient = new MountebankClient();
}
/**
* Create stateful mock that changes behavior based on previous calls
*/
public void createStatefulHttpMock(int port) throws Exception {
Map<String, Object> imposter = Map.of(
"port", port,
"protocol", "http",
"name", "Stateful HTTP Mock",
"stubs", new Object[]{
createLoginStub(),
createAuthenticatedStub(),
createLogoutStub()
}
);
mbClient.createImposter(imposter);
}
private Map<String, Object> createLoginStub() {
return Map.of(
"predicates", new Object[]{
Map.of("and", new Object[]{
Map.of("equals", Map.of("method", "POST")),
Map.of("equals", Map.of("path", "/api/login"))
})
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"headers", Map.of(
"Content-Type", "application/json",
"Set-Cookie", "sessionId=abc123; Path=/"
),
"body": """
{
"status": "success",
"message": "Login successful",
"user": {"id": 1, "name": "Test User"}
}
"""
))
}
);
}
private Map<String, Object> createAuthenticatedStub() {
return Map.of(
"predicates", new Object[]{
Map.of("and", new Object[]{
Map.of("equals", Map.of("method", "GET")),
Map.of("equals", Map.of("path", "/api/profile")),
Map.of("contains", Map.of(
"headers", Map.of("Cookie", "sessionId=abc123")
))
})
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"body": """
{
"user": {"id": 1, "name": "Test User", "email": "[email protected]"},
"preferences": {"theme": "dark", "language": "en"}
}
"""
))
}
);
}
private Map<String, Object> createLogoutStub() {
return Map.of(
"predicates", new Object[]{
Map.of("and", new Object[]{
Map.of("equals", Map.of("method", "POST")),
Map.of("equals", Map.of("path", "/api/logout"))
})
},
"responses", new Object[]{
Map.of("is", Map.of(
"statusCode", 200,
"headers", Map.of(
"Set-Cookie", "sessionId=; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
),
"body": """
{"status": "success", "message": "Logged out successfully"}
"""
))
}
);
}
}
CI/CD Integration
GitHub Actions Workflow
name: Java Tests with Mountebank on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Java uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install Mountebank run: npm install -g mountebank - name: Start Mountebank run: | mb --port 2525 --allowInjection & sleep 5 # Wait for Mountebank to start - name: Build with Maven run: mvn clean package - name: Run tests run: mvn test
Docker Compose for Local Development
version: '3.8' services: mountebank: image: bbyars/mountebank:2.8.1 ports: - "2525:2525" - "4545:4545" - "5555:5555" - "6666:6666" networks: - test-network java-app: build: . ports: - "8080:8080" depends_on: - mountebank environment: - EXTERNAL_API_URL=http://mountebank:4545 networks: - test-network networks: test-network: driver: bridge
Best Practices
- Use Descriptive Names: Name your imposters clearly for debugging
- Clean Up Resources: Always delete imposters after tests
- Use Multiple Ports: Different tests should use different ports
- Validate Mock Setup: Verify imposters are created successfully
- Test Error Scenarios: Mock error responses and edge cases
- Use Stateful Mocks Sparingly: They can make tests more complex
Conclusion
Mountebank provides Java developers with a powerful, flexible tool for creating comprehensive protocol mocks that go far beyond simple HTTP stubs. Key advantages include:
- True Protocol Simulation: Mock HTTP, HTTPS, TCP, and SMTP with real protocol behavior
- Language Independence: Mocks work consistently across different services and languages
- Complex Behaviors: Support for stateful mocks, predicates, and dynamic responses
- CI/CD Friendly: Easy to integrate into automated testing pipelines
- Developer Experience: REST API makes it easy to programmatically control mocks
For Java teams building microservices or integrating with external systems, Mountebank offers enterprise-grade service virtualization that significantly improves test reliability, speed, and coverage while reducing dependencies on unstable external services.