Lesson 12 of 14 ~90 min
Course progress
0%

Service Virtualization with WireMock

Master service virtualization techniques using WireMock to simulate complex microservices scenarios

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:

  1. Create WireMock stubs for a microservices ecosystem
  2. Implement stateful shopping cart scenario
  3. Add fault injection for network delays and failures
  4. Record real service interactions for playback
  5. Build reusable stub manager for TestNG
  6. Verify API calls in your E2E tests

Next: Learn distributed tracing in E2E tests →