Comprehensive Hoverfly Implementation Guide
1. Hoverfly Configuration and Setup
Maven Dependencies
<!-- pom.xml -->
<properties>
<hoverfly.version>0.14.3</hoverfly.version>
<wiremock.version>2.35.0</wiremock.version>
</properties>
<dependencies>
<!-- Hoverfly Java DSL -->
<dependency>
<groupId>io.specto</groupId>
<artifactId>hoverfly-java</artifactId>
<version>${hoverfly.version}</version>
<scope>test</scope>
</dependency>
<!-- Hoverfly JUnit5 Extension -->
<dependency>
<groupId>io.specto</groupId>
<artifactId>hoverfly-junit5</artifactId>
<version>${hoverfly.version}</version>
<scope>test</scope>
</dependency>
<!-- Spring Test for Integration Tests -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- WireMock for Comparison -->
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
2. Hoverfly Configuration Class
// HoverflyConfig.java
package com.example.hoverfly.config;
import io.specto.hoverfly.junit.core.Hoverfly;
import io.specto.hoverfly.junit.core.HoverflyConfig;
import io.specto.hoverfly.junit.core.HoverflyMode;
import io.specto.hoverfly.junit.core.SimulationSource;
import io.specto.hoverfly.junit5.HoverflyExtension;
import io.specto.hoverfly.junit5.api.HoverflyConfig;
import io.specto.hoverfly.junit5.api.HoverflyCore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import java.nio.file.Paths;
@Configuration
public class HoverflyConfig {
@Value("${hoverfly.admin.port:8888}")
private int adminPort;
@Value("${hoverfly.proxy.port:8500}")
private int proxyPort;
@Value("${hoverfly.simulation.directory:src/test/resources/hoverfly}")
private String simulationDirectory;
@Bean
@Profile("test")
public Hoverfly hoverfly() {
HoverflyConfig config = HoverflyConfig.localConfigs()
.adminPort(adminPort)
.proxyPort(proxyPort)
.captureHeaders("Content-Type", "Authorization")
.enableStatefulCapture(10)
.plainHttpTunneling()
.logToStdOut();
return new Hoverfly(config, HoverflyMode.SIMULATE);
}
@Bean
@Profile("capture")
public Hoverfly hoverflyCapture() {
HoverflyConfig config = HoverflyConfig.localConfigs()
.adminPort(adminPort)
.proxyPort(proxyPort)
.captureHeaders("Content-Type", "Authorization", "X-Correlation-ID")
.enableStatefulCapture(50)
.destination("api.github.com")
.destination("jsonplaceholder.typicode.com")
.plainHttpTunneling();
return new Hoverfly(config, HoverflyMode.CAPTURE);
}
}
3. Service Virtualization Manager
// HoverflyService.java
package com.example.hoverfly.service;
import io.specto.hoverfly.junit.core.Hoverfly;
import io.specto.hoverfly.junit.core.SimulationSource;
import io.specto.hoverfly.junit.core.model.RequestFieldMatcher;
import io.specto.hoverfly.junit.core.model.Response;
import io.specto.hoverfly.junit.core.model.ResponseBuilder;
import io.specto.hoverfly.junit.core.model.DelaySettings;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class HoverflyService {
private static final Logger logger = LoggerFactory.getLogger(HoverflyService.class);
private final Hoverfly hoverfly;
private final ObjectMapper objectMapper;
private final ResourceLoader resourceLoader;
private final Map<String, SimulationSource> simulationCache = new ConcurrentHashMap<>();
@Autowired
public HoverflyService(Hoverfly hoverfly, ObjectMapper objectMapper, ResourceLoader resourceLoader) {
this.hoverfly = hoverfly;
this.objectMapper = objectMapper;
this.resourceLoader = resourceLoader;
}
public void startHoverfly() {
if (!hoverfly.isRunning()) {
hoverfly.start();
logger.info("Hoverfly started on proxy port: {}", hoverfly.getHoverflyConfig().getProxyPort());
}
}
public void stopHoverfly() {
if (hoverfly.isRunning()) {
hoverfly.close();
logger.info("Hoverfly stopped");
}
}
public void importSimulation(String simulationName) {
try {
String simulationPath = String.format("classpath:hoverfly/%s.json", simulationName);
Resource resource = resourceLoader.getResource(simulationPath);
if (resource.exists()) {
SimulationSource source = SimulationSource.defaultPath(resource.getFile().toPath());
hoverfly.importSimulation(source);
simulationCache.put(simulationName, source);
logger.info("Imported simulation: {}", simulationName);
} else {
logger.warn("Simulation file not found: {}", simulationPath);
}
} catch (IOException e) {
logger.error("Failed to import simulation: {}", simulationName, e);
}
}
public void exportSimulation(String simulationName) {
try {
String exportPath = String.format("src/test/resources/hoverfly/%s-exported-%s.json",
simulationName, LocalDateTime.now().format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME));
hoverfly.exportSimulation(Paths.get(exportPath));
logger.info("Exported simulation to: {}", exportPath);
} catch (IOException e) {
logger.error("Failed to export simulation: {}", simulationName, e);
}
}
public void simulateGetRequest(String service, String path, int statusCode, String responseBody) {
simulateGetRequest(service, path, statusCode, responseBody, Collections.emptyMap());
}
public void simulateGetRequest(String service, String path, int statusCode,
String responseBody, Map<String, String> headers) {
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.get(RequestFieldMatcher.newGlobMatcher(path))
.willReturn(ResponseBuilder.response()
.status(statusCode)
.body(responseBody)
.headers(headers)
.build())
));
logger.debug("Simulated GET {} with status: {}", path, statusCode);
}
public void simulatePostRequest(String service, String path, String requestBody,
int statusCode, String responseBody) {
simulatePostRequest(service, path, requestBody, statusCode, responseBody, Collections.emptyMap());
}
public void simulatePostRequest(String service, String path, String requestBody,
int statusCode, String responseBody, Map<String, String> headers) {
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.post(RequestFieldMatcher.newGlobMatcher(path))
.body(RequestFieldMatcher.newExactMatcher(requestBody))
.willReturn(ResponseBuilder.response()
.status(statusCode)
.body(responseBody)
.headers(headers)
.build())
));
logger.debug("Simulated POST {} with status: {}", path, statusCode);
}
public void simulateWithDelay(String service, String path, String method,
int statusCode, String responseBody, int delayMs) {
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.andMethod(method)
.andPath(path)
.willReturn(ResponseBuilder.response()
.status(statusCode)
.body(responseBody)
.header("Content-Type", "application/json")
.andSetDelay(DelaySettings.delay(delayMs).build())
.build())
));
logger.debug("Simulated {} {} with {}ms delay", method, path, delayMs);
}
public void simulateErrorScenario(String service, String path, String method,
int statusCode, String errorMessage) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", LocalDateTime.now().toString());
errorResponse.put("status", statusCode);
errorResponse.put("error", getHttpStatusText(statusCode));
errorResponse.put("message", errorMessage);
errorResponse.put("path", path);
try {
String responseBody = objectMapper.writeValueAsString(errorResponse);
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.andMethod(method)
.andPath(path)
.willReturn(ResponseBuilder.response()
.status(statusCode)
.body(responseBody)
.header("Content-Type", "application/json")
.build())
));
logger.debug("Simulated error: {} {} -> {}", method, path, statusCode);
} catch (Exception e) {
logger.error("Failed to simulate error scenario", e);
}
}
public void simulateStatefulBehavior(String service, String path, String initialState,
Map<String, String> stateTransitions) {
// Implement stateful simulation
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.get(path)
.willReturn(ResponseBuilder.response()
.status(200)
.body(String.format("{\"state\": \"%s\"}", initialState))
.build())
));
// Add state transitions
stateTransitions.forEach((trigger, newState) -> {
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.post(path)
.body(RequestFieldMatcher.newJsonPartialMatcher(
String.format("{\"action\": \"%s\"}", trigger)))
.willReturn(ResponseBuilder.response()
.status(200)
.body(String.format("{\"state\": \"%s\"}", newState))
.build())
));
});
}
public Map<String, Object> getHoverflyMetrics() {
Map<String, Object> metrics = new HashMap<>();
metrics.put("isRunning", hoverfly.isRunning());
metrics.put("mode", hoverfly.getMode().name());
metrics.put("proxyPort", hoverfly.getHoverflyConfig().getProxyPort());
metrics.put("adminPort", hoverfly.getHoverflyConfig().getAdminPort());
metrics.put("cachedSimulations", simulationCache.size());
return metrics;
}
private String getHttpStatusText(int statusCode) {
switch (statusCode) {
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 403: return "Forbidden";
case 404: return "Not Found";
case 500: return "Internal Server Error";
case 503: return "Service Unavailable";
default: return "Error";
}
}
}
4. External Service Client with Hoverfly
// ExternalServiceClient.java
package com.example.hoverfly.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Component
public class ExternalServiceClient {
private static final Logger logger = LoggerFactory.getLogger(ExternalServiceClient.class);
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
@Value("${external.service.base-url:http://localhost:8500}")
private String baseUrl;
@Value("${external.service.timeout:5000}")
private int timeout;
@Autowired
public ExternalServiceClient(RestTemplate restTemplate, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
public <T> T get(String path, Class<T> responseType) {
return get(path, responseType, Collections.emptyMap());
}
public <T> T get(String path, Class<T> responseType, Map<String, String> headers) {
String url = baseUrl + path;
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::set);
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(httpHeaders);
try {
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.GET, entity, responseType);
logger.debug("GET {} -> {}", url, response.getStatusCode());
return response.getBody();
} catch (Exception e) {
logger.error("GET {} failed: {}", url, e.getMessage());
throw new ExternalServiceException("GET request failed", e);
}
}
public <T> T post(String path, Object request, Class<T> responseType) {
return post(path, request, responseType, Collections.emptyMap());
}
public <T> T post(String path, Object request, Class<T> responseType,
Map<String, String> headers) {
String url = baseUrl + path;
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::set);
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String requestBody;
try {
requestBody = objectMapper.writeValueAsString(request);
} catch (Exception e) {
throw new ExternalServiceException("Failed to serialize request", e);
}
HttpEntity<String> entity = new HttpEntity<>(requestBody, httpHeaders);
try {
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.POST, entity, responseType);
logger.debug("POST {} -> {}", url, response.getStatusCode());
return response.getBody();
} catch (Exception e) {
logger.error("POST {} failed: {}", url, e.getMessage());
throw new ExternalServiceException("POST request failed", e);
}
}
public <T> T put(String path, Object request, Class<T> responseType) {
String url = baseUrl + path;
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String requestBody;
try {
requestBody = objectMapper.writeValueAsString(request);
} catch (Exception e) {
throw new ExternalServiceException("Failed to serialize request", e);
}
HttpEntity<String> entity = new HttpEntity<>(requestBody, httpHeaders);
try {
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.PUT, entity, responseType);
logger.debug("PUT {} -> {}", url, response.getStatusCode());
return response.getBody();
} catch (Exception e) {
logger.error("PUT {} failed: {}", url, e.getMessage());
throw new ExternalServiceException("PUT request failed", e);
}
}
public void delete(String path) {
String url = baseUrl + path;
try {
restTemplate.delete(url);
logger.debug("DELETE {} -> Success", url);
} catch (Exception e) {
logger.error("DELETE {} failed: {}", url, e.getMessage());
throw new ExternalServiceException("DELETE request failed", e);
}
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public static class ExternalServiceException extends RuntimeException {
public ExternalServiceException(String message) {
super(message);
}
public ExternalServiceException(String message, Throwable cause) {
super(message, cause);
}
}
}
5. JUnit 5 Test Examples
// UserServiceHoverflyTest.java
package com.example.hoverfly.test;
import com.example.hoverfly.client.ExternalServiceClient;
import com.example.hoverfly.service.HoverflyService;
import io.specto.hoverfly.junit5.HoverflyExtension;
import io.specto.hoverfly.junit5.api.HoverflyConfig;
import io.specto.hoverfly.junit5.api.HoverflyCore;
import io.specto.hoverfly.junit5.api.HoverflySimulate;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ExtendWith(HoverflyExtension.class)
@HoverflyCore(config = @HoverflyConfig(adminPort = 8888, proxyPort = 8500))
class UserServiceHoverflyTest {
@Autowired
private ExternalServiceClient externalServiceClient;
@Autowired
private HoverflyService hoverflyService;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
// Point client to Hoverfly proxy
externalServiceClient.setBaseUrl("http://localhost:8500");
}
@Test
@HoverflySimulate(source = @HoverflySimulate.Source(value = "classpath:hoverfly/user-service.json", type = HoverflySimulate.SourceType.FILE))
void testGetUserSuccess() {
// When
Map<String, Object> user = externalServiceClient.get("/users/1", HashMap.class);
// Then
assertNotNull(user);
assertEquals(1, user.get("id"));
assertEquals("John Doe", user.get("name"));
assertEquals("[email protected]", user.get("email"));
}
@Test
void testGetUserWithProgrammaticSimulation() {
// Given
String userJson = """
{
"id": 123,
"name": "Jane Smith",
"email": "[email protected]",
"active": true
}
""";
hoverflyService.simulateGetRequest("jsonplaceholder.typicode.com",
"/users/123", 200, userJson);
// When
Map<String, Object> user = externalServiceClient.get("/users/123", HashMap.class);
// Then
assertNotNull(user);
assertEquals(123, user.get("id"));
assertEquals("Jane Smith", user.get("name"));
assertTrue((Boolean) user.get("active"));
}
@Test
void testCreateUser() {
// Given
String requestBody = """
{
"name": "New User",
"email": "[email protected]"
}
""";
String responseBody = """
{
"id": 999,
"name": "New User",
"email": "[email protected]",
"createdAt": "2024-01-15T10:30:00Z"
}
""";
Map<String, String> headers = new HashMap<>();
headers.put("Location", "http://localhost:8500/users/999");
hoverflyService.simulatePostRequest("jsonplaceholder.typicode.com",
"/users", requestBody, 201, responseBody, headers);
Map<String, Object> userRequest = new HashMap<>();
userRequest.put("name", "New User");
userRequest.put("email", "[email protected]");
// When
Map<String, Object> createdUser = externalServiceClient.post(
"/users", userRequest, HashMap.class);
// Then
assertNotNull(createdUser);
assertEquals(999, createdUser.get("id"));
assertEquals("New User", createdUser.get("name"));
}
@Test
void testGetUserNotFound() {
// Given
hoverflyService.simulateErrorScenario("jsonplaceholder.typicode.com",
"/users/999", "GET", 404, "User not found");
// When & Then
ExternalServiceClient.ExternalServiceException exception =
assertThrows(ExternalServiceClient.ExternalServiceException.class,
() -> externalServiceClient.get("/users/999", HashMap.class));
assertTrue(exception.getMessage().contains("GET request failed"));
}
@Test
void testServiceUnavailable() {
// Given
hoverflyService.simulateErrorScenario("jsonplaceholder.typicode.com",
"/users", "GET", 503, "Service temporarily unavailable");
// When & Then
ExternalServiceClient.ExternalServiceException exception =
assertThrows(ExternalServiceClient.ExternalServiceException.class,
() -> externalServiceClient.get("/users", HashMap.class));
assertTrue(exception.getMessage().contains("GET request failed"));
}
@Test
void testSlowResponse() {
// Given - Simulate a slow response with 2 second delay
String usersJson = """
[
{"id": 1, "name": "User 1"},
{"id": 2, "name": "User 2"}
]
""";
hoverflyService.simulateWithDelay("jsonplaceholder.typicode.com",
"/users", "GET", 200, usersJson, 2000);
long startTime = System.currentTimeMillis();
// When
Object users = externalServiceClient.get("/users", Object.class);
long endTime = System.currentTimeMillis();
// Then
assertNotNull(users);
long duration = endTime - startTime;
assertTrue(duration >= 2000, "Response should take at least 2 seconds");
}
}
6. Simulation Source Files
User Service Simulation
{
"data": {
"pairs": [
{
"request": {
"path": [
{
"matcher": "exact",
"value": "/users/1"
}
],
"method": [
{
"matcher": "exact",
"value": "GET"
}
],
"destination": [
{
"matcher": "exact",
"value": "jsonplaceholder.typicode.com"
}
],
"scheme": [
{
"matcher": "exact",
"value": "http"
}
]
},
"response": {
"status": 200,
"body": "{\n \"id\": 1,\n \"name\": \"John Doe\",\n \"username\": \"johndoe\",\n \"email\": \"[email protected]\",\n \"address\": {\n \"street\": \"123 Main St\",\n \"city\": \"Springfield\",\n \"zipcode\": \"12345\"\n },\n \"phone\": \"1-555-123-4567\",\n \"website\": \"johndoe.com\"\n}",
"encodedBody": false,
"headers": {
"Content-Type": [
"application/json"
],
"Date": [
"Mon, 15 Jan 2024 10:00:00 GMT"
]
}
}
},
{
"request": {
"path": [
{
"matcher": "exact",
"value": "/users"
}
],
"method": [
{
"matcher": "exact",
"value": "POST"
}
],
"destination": [
{
"matcher": "exact",
"value": "jsonplaceholder.typicode.com"
}
],
"scheme": [
{
"matcher": "exact",
"value": "http"
}
],
"body": [
{
"matcher": "json",
"value": "{\"name\":\"New User\",\"email\":\"[email protected]\"}"
}
]
},
"response": {
"status": 201,
"body": "{\n \"id\": 999,\n \"name\": \"New User\",\n \"email\": \"[email protected]\",\n \"createdAt\": \"2024-01-15T10:30:00Z\"\n}",
"encodedBody": false,
"headers": {
"Content-Type": [
"application/json"
],
"Location": [
"http://localhost:8500/users/999"
]
}
}
}
],
"globalActions": {
"delays": []
}
},
"meta": {
"schemaVersion": "v5",
"hoverflyVersion": "v1.0.0",
"timeExported": "2024-01-15T10:00:00Z"
}
}
7. Advanced Hoverfly Features
Stateful Simulation Service
// StatefulSimulationService.java
package com.example.hoverfly.service;
import io.specto.hoverfly.junit.core.Hoverfly;
import io.specto.hoverfly.junit.core.SimulationSource;
import io.specto.hoverfly.junit.core.model.RequestFieldMatcher;
import io.specto.hoverfly.junit.core.model.ResponseBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class StatefulSimulationService {
private final Hoverfly hoverfly;
private final Map<String, AtomicInteger> requestCounters = new HashMap<>();
private final Map<String, String> sessionStates = new HashMap<>();
@Autowired
public StatefulSimulationService(Hoverfly hoverfly) {
this.hoverfly = hoverfly;
}
public void simulateRateLimitedEndpoint(String service, String path,
int limit, int windowSeconds) {
String counterKey = service + path;
requestCounters.putIfAbsent(counterKey, new AtomicInteger(0));
// This is a simplified implementation
// In production, you'd want more sophisticated rate limiting simulation
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.get(path)
.willReturn(ResponseBuilder.response()
.status(200)
.body("{\"message\": \"Success\"}")
.build()),
SimulationSource.service(service)
.get(path)
.willReturn(ResponseBuilder.response()
.status(429)
.body("{\"error\": \"Rate limit exceeded\", \"retryAfter\": \"60\"}")
.build())
.withWeight(10) // Higher weight for success cases
));
}
public void simulateSessionBasedBehavior(String service, String basePath) {
// Initial state - not authenticated
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.get(basePath + "/profile")
.willReturn(ResponseBuilder.response()
.status(401)
.body("{\"error\": \"Not authenticated\"}")
.build())
));
// Login endpoint
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.post(basePath + "/login")
.body(RequestFieldMatcher.newJsonPartialMatcher("{\"username\":"))
.willReturn(ResponseBuilder.response()
.status(200)
.body("{\"token\": \"abc123\", \"expiresIn\": 3600}")
.header("Set-Cookie", "session=abc123; HttpOnly")
.build())
));
// Authenticated profile access
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.get(basePath + "/profile")
.header("Authorization", RequestFieldMatcher.newGlobMatcher("Bearer *"))
.willReturn(ResponseBuilder.response()
.status(200)
.body("{\"username\": \"testuser\", \"email\": \"[email protected]\"}")
.build())
));
}
public void simulateCircuitBreakerScenario(String service, String path) {
// Simulate a service that fails initially but recovers
hoverfly.simulate(SimulationSource.dsl(
SimulationSource.service(service)
.get(path)
.willReturn(ResponseBuilder.response()
.status(503)
.body("{\"error\": \"Service unavailable\"}")
.build()),
SimulationSource.service(service)
.get(path)
.willReturn(ResponseBuilder.response()
.status(200)
.body("{\"status\": \"healthy\"}")
.build())
.withWeight(3) // Recovery case has lower weight initially
));
}
}
8. Integration Test Configuration
// TestConfig.java
package com.example.hoverfly.config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@TestConfiguration
public class TestConfig {
@Bean
public RestTemplate testRestTemplate() {
return new RestTemplate();
}
@Bean
public HoverflySimulationManager hoverflySimulationManager() {
return new HoverflySimulationManager();
}
}
// HoverflySimulationManager.java
package com.example.hoverfly.config;
import io.specto.hoverfly.junit.core.Hoverfly;
import io.specto.hoverfly.junit.core.HoverflyConfig;
import io.specto.hoverfly.junit.core.HoverflyMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
@Component
public class HoverflySimulationManager {
private static final Logger logger = LoggerFactory.getLogger(HoverflySimulationManager.class);
private Hoverfly hoverfly;
public void startSimulation() {
if (hoverfly == null) {
HoverflyConfig config = HoverflyConfig.localConfigs()
.adminPort(8888)
.proxyPort(8500)
.captureHeaders("Content-Type", "Authorization", "X-Request-ID")
.plainHttpTunneling();
hoverfly = new Hoverfly(config, HoverflyMode.SIMULATE);
hoverfly.start();
logger.info("Hoverfly simulation started");
}
}
public void startCapture() {
if (hoverfly == null) {
HoverflyConfig config = HoverflyConfig.localConfigs()
.adminPort(8888)
.proxyPort(8500)
.captureHeaders("Content-Type", "Authorization", "X-Correlation-ID")
.destination("api.github.com")
.destination("jsonplaceholder.typicode.com")
.plainHttpTunneling();
hoverfly = new Hoverfly(config, HoverflyMode.CAPTURE);
hoverfly.start();
logger.info("Hoverfly capture mode started");
}
}
@PreDestroy
public void stop() {
if (hoverfly != null && hoverfly.isRunning()) {
hoverfly.close();
logger.info("Hoverfly stopped");
}
}
public Hoverfly getHoverfly() {
return hoverfly;
}
}
This comprehensive Hoverfly implementation provides:
- Service virtualization for external dependencies
- Programmatic simulation configuration
- Stateful behavior simulation
- Error scenario testing
- Performance testing with delays
- JUnit 5 integration
- Spring Boot compatibility
- Simulation management and export capabilities
The setup allows you to test your Java applications reliably without depending on external services, enabling faster development cycles and more comprehensive testing.