Service Virtualization with WireMock
Learn advanced service virtualization patterns to create realistic test environments for complex microservices architectures.
WireMock Fundamentals
WireMock is a flexible API mocking tool that allows you to simulate HTTP services for testing purposes.
Maven Setup
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
<version>2.35.0</version>
<scope>test</scope>
</dependency>
Advanced Stubbing Patterns
Dynamic Response Generation
public class AdvancedWireMockTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(options()
.port(8089)
.extensions(new ResponseTemplateTransformer(true)));
@Test
void testDynamicPricing() {
// Stub with template response
stubFor(get(urlPathEqualTo("/api/pricing"))
.withQueryParam("productId", matching("[0-9]+"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"productId": "{{request.query.productId}}",
"price": {{randomValue type='DECIMAL' min=10.00 max=100.00}},
"currency": "USD",
"timestamp": "{{now format='yyyy-MM-dd HH:mm:ss'}}"
}
""")
.withTransformers("response-template")));
// Selenium test
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:8080/product/12345");
WebElement price = driver.findElement(By.className("product-price"));
assertNotNull(price.getText());
driver.quit();
}
}
Stateful Behavior
@Test
void testStatefulShoppingCart() {
// Initial empty cart
stubFor(get("/api/cart")
.inScenario("Shopping Cart")
.whenScenarioStateIs(STARTED)
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"items\": [], \"total\": 0}")
)
);
// After adding item
stubFor(post("/api/cart/items")
.inScenario("Shopping Cart")
.whenScenarioStateIs(STARTED)
.willSetStateTo("Item Added")
.willReturn(aResponse().withStatus(201))
);
stubFor(get("/api/cart")
.inScenario("Shopping Cart")
.whenScenarioStateIs("Item Added")
.willReturn(aResponse()
.withStatus(200)
.withBody("""
{
"items": [{"id": "123", "name": "Product", "price": 29.99}],
"total": 29.99
}
""")
)
);
// Selenium test flow
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:8080");
// Verify empty cart
assertEquals("0", driver.findElement(By.id("cart-count")).getText());
// Add item
driver.findElement(By.id("add-to-cart")).click();
// Verify cart updated
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
wait.until(driver ->
driver.findElement(By.id("cart-count")).getText().equals("1"));
driver.quit();
}
Fault Injection
Network Delays
@Test
void testSlowServiceResponse() {
stubFor(get("/api/recommendations")
.willReturn(aResponse()
.withStatus(200)
.withFixedDelay(5000) // 5 second delay
.withBody("[{\"id\": 1, \"name\": \"Recommended Product\"}]")
)
);
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:8080/products");
// Verify loading indicator appears
WebElement loader = driver.findElement(By.className("loading-spinner"));
assertTrue(loader.isDisplayed());
// Wait for slow service
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.invisibilityOf(loader));
// Verify recommendations loaded
WebElement recommendations = driver.findElement(By.id("recommendations"));
assertFalse(recommendations.findElements(By.tagName("li")).isEmpty());
driver.quit();
}
Service Failures
@Test
void testServiceFailureHandling() {
// Stub payment service failure
stubFor(post("/api/payment")
.willReturn(aResponse()
.withStatus(503)
.withBody("{\"error\": \"Payment service unavailable\"}")
)
);
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:8080/checkout");
// Fill payment form
driver.findElement(By.id("card-number")).sendKeys("4111111111111111");
driver.findElement(By.id("submit-payment")).click();
// Verify error handling
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
WebElement errorMsg = wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.className("error-message")
)
);
assertTrue(errorMsg.getText().contains("unavailable"));
driver.quit();
}
Request Matching
Complex URL Patterns
@Test
void testComplexUrlMatching() {
// Match search with filters
stubFor(get(urlPathMatching("/api/products/search"))
.withQueryParam("q", matching(".*laptop.*"))
.withQueryParam("minPrice", matching("[0-9]+"))
.withQueryParam("maxPrice", matching("[0-9]+"))
.withQueryParam("brand", matching("(Dell|HP|Lenovo)"))
.willReturn(aResponse()
.withStatus(200)
.withBodyFile("search-results-laptops.json"))
);
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:8080/search?q=laptop&minPrice=500&maxPrice=1500&brand=Dell");
List<WebElement> results = driver.findElements(By.className("product-item"));
assertFalse(results.isEmpty());
driver.quit();
}
Header-Based Routing
@Test
void testApiVersioning() {
// API v1
stubFor(get("/api/users/123")
.withHeader("Accept", equalTo("application/vnd.api.v1+json"))
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"id\": \"123\", \"name\": \"John Doe\"}")
)
);
// API v2 with additional fields
stubFor(get("/api/users/123")
.withHeader("Accept", equalTo("application/vnd.api.v2+json"))
.willReturn(aResponse()
.withStatus(200)
.withBody("""
{
"id": "123",
"name": "John Doe",
"email": "john@example.com",
"avatar": "https://example.com/avatar.jpg"
}
""")
)
);
}
Recording and Playback
Proxy Recording
public class WireMockRecorder {
@Test
void recordRealServiceInteractions() {
WireMockServer wireMock = new WireMockServer(options()
.port(8089)
.enableBrowserProxying(true)
.proxyVia("real-service.example.com", 443));
wireMock.start();
// Enable recording
wireMock.startRecording(recordSpec()
.forTarget("https://real-service.example.com")
.captureHeader("Accept")
.captureHeader("Content-Type")
.extractTextBodiesOver(1024)
.build());
// Run Selenium test against proxy
ChromeOptions options = new ChromeOptions();
options.setProxy(new Proxy()
.setHttpProxy("localhost:8089")
.setSslProxy("localhost:8089"));
WebDriver driver = new ChromeDriver(options);
driver.get("http://localhost:8080/products");
// Perform actions...
driver.findElement(By.id("search")).sendKeys("laptop");
driver.findElement(By.id("search-btn")).click();
driver.quit();
// Stop recording and save
SnapshotRecordResult recordings = wireMock.stopRecording();
wireMock.saveMappings();
System.out.println("Recorded " + recordings.getStubMappings().size() + " interactions");
wireMock.stop();
}
}
Advanced Response Templates
Request-Based Responses
stubFor(post("/api/orders")
.willReturn(aResponse()
.withStatus(201)
.withHeader("Location", "/api/orders/{{randomValue type='UUID'}}")
.withBody("""
{
"orderId": "{{randomValue type='UUID'}}",
"customerId": "{{jsonPath request.body '$.customerId'}}",
"items": {{jsonPath request.body '$.items'}},
"total": {{jsonPath request.body '$.total'}},
"status": "pending",
"createdAt": "{{now}}"
}
""")
.withTransformers("response-template"))
);
Date/Time Helpers
stubFor(get("/api/reports/daily")
.willReturn(aResponse()
.withBody("""
{
"reportDate": "{{now format='yyyy-MM-dd'}}",
"reportTime": "{{now format='HH:mm:ss'}}",
"previousDay": "{{now offset='-1 days' format='yyyy-MM-dd'}}",
"nextWeek": "{{now offset='7 days' format='yyyy-MM-dd'}}"
}
""")
.withTransformers("response-template"))
);
Integration with Test Framework
Reusable Stub Manager
public class MicroserviceStubManager {
private final WireMockServer wireMock;
private final Map<String, String> serviceEndpoints;
public MicroserviceStubManager() {
this.wireMock = new WireMockServer(options()
.dynamicPort()
.extensions(new ResponseTemplateTransformer(true)));
this.serviceEndpoints = new HashMap<>();
}
public void start() {
wireMock.start();
WireMock.configureFor("localhost", wireMock.port());
}
public void registerService(String serviceName, String stubsPath) {
wireMock.loadMappingsFrom(stubsPath);
serviceEndpoints.put(serviceName, "http://localhost:" + wireMock.port());
}
public String getServiceUrl(String serviceName) {
return serviceEndpoints.get(serviceName);
}
public void reset() {
wireMock.resetAll();
}
public void stop() {
wireMock.stop();
}
public void verify(int count, RequestPatternBuilder pattern) {
wireMock.verify(count, pattern);
}
}
TestNG Integration
public class E2ETestBase {
protected MicroserviceStubManager stubManager;
protected WebDriver driver;
@BeforeClass
public void setupStubs() {
stubManager = new MicroserviceStubManager();
stubManager.start();
stubManager.registerService("UserService", "stubs/user-service");
stubManager.registerService("ProductService", "stubs/product-service");
stubManager.registerService("OrderService", "stubs/order-service");
}
@BeforeMethod
public void setupDriver() {
driver = new ChromeDriver();
// Configure app to use stub services
System.setProperty("services.user.url", stubManager.getServiceUrl("UserService"));
System.setProperty("services.product.url", stubManager.getServiceUrl("ProductService"));
}
@AfterMethod
public void teardownDriver() {
if (driver != null) {
driver.quit();
}
stubManager.reset();
}
@AfterClass
public void teardownStubs() {
stubManager.stop();
}
}
Key Takeaways
- WireMock enables sophisticated API mocking
- Use stateful scenarios for complex workflows
- Inject faults to test error handling
- Record real services for realistic stubs
- Template responses for dynamic data
- Integrate stubs into your test framework
- Verify API calls during tests
Practice Exercise
Build a comprehensive service virtualization layer:
- Create WireMock stubs for a microservices ecosystem
- Implement stateful shopping cart scenario
- Add fault injection for network delays and failures
- Record real service interactions for playback
- Build reusable stub manager for TestNG
- Verify API calls in your E2E tests
Next: Learn distributed tracing in E2E tests →