Styrow.dev
Question 77 of 82
Selenium-Java Topic: Backend State Simulation hard

Advanced Selenium: Simulating Backend States for UI Testing

Question

Advanced Selenium: Simulating Backend States for UI Testing

Answer

You’re a Staff QA Engineer tasked with automating tests for a feature-rich e-commerce application. A critical component is the product detail page (PDP) and checkout flow, where UI elements (e.g., ‘Add to Cart’ button, pricing, promotional banners, payment confirmation) dynamically react to various backend states:

  1. Inventory Levels: An item out of stock should disable ‘Add to Cart’. Low stock might show a warning.
  2. Promotional Eligibility: Discounts apply only when specific criteria are met (e.g., cart value, user group), altering displayed prices.
  3. Payment Gateway Responses: Successful payments should show a confirmation, but a declined payment should show an error, and a ‘pending’ state might display a loading spinner for a few seconds.

Designing robust end-to-end tests for these scenarios using Selenium-Java is challenging. Directly manipulating the actual backend or database for each test creates setup/teardown complexities, race conditions in shared environments, and slow execution.

Your task: Propose and implement a production-grade automated testing strategy using Selenium-Java to reliably simulate these diverse backend states. Provide concrete, re-usable code examples for two specific scenarios:

  1. Out-of-Stock Scenario: Simulate an API response that indicates a product is out of stock, then verify the UI correctly disables the ‘Add to Cart’ button.
  2. Delayed Successful Payment: Simulate a payment gateway API response that indicates a ‘pending’ state followed by a ‘success’ state after a brief delay, and verify the UI’s loading indicator and final confirmation.

Automating tests for UIs highly dependent on dynamic backend states presents a significant challenge for speed, reliability, and isolation. Modifying the actual backend for each test leads to slow execution, non-deterministic results, and complex test data management. A production-grade strategy for Selenium-Java involves intercepting and modifying network traffic between the browser and the backend using a proxy. BrowserMob Proxy is an excellent tool for this, allowing granular control over HTTP/HTTPS requests and responses.

Strategy Overview: BrowserMob Proxy Integration

BrowserMob Proxy acts as an HTTP proxy that Selenium WebDriver can be configured to use. It allows us to:

  1. Start and Stop a Proxy Server: Managed programmatically within test setup and teardown.
  2. Capture Network Traffic: Monitor all requests and responses.
  3. Manipulate Requests/Responses: Dynamically modify headers, status codes, and body content of specific API calls.
  4. Simulate Delays: Introduce artificial delays to responses to test loading states.

This approach ensures test isolation by running each test with its own specific mocked backend state, significantly speeds up test execution by avoiding real backend calls, and improves reliability by eliminating external dependencies.

Implementation Steps:

  1. Add Dependencies: Include browsermob-core, selenium-java, and a test framework like TestNG or JUnit in your pom.xml.

    <dependencies>
        <!-- Selenium WebDriver -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>4.11.0</version> <!-- Use a recent version -->
        </dependency>
        <!-- BrowserMob Proxy -->
        <dependency>
            <groupId>net.lightbody.bmp</groupId>
            <artifactId>browsermob-core</artifactId>
            <version>2.1.5</version> <!-- Use a recent version -->
            <scope>test</scope>
        </dependency>
        <!-- TestNG (or JUnit) -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>7.8.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
  2. Basic Proxy and WebDriver Setup: A base test class BaseTest will manage the proxy lifecycle and WebDriver initialization.

    import net.lightbody.bmp.BrowserMobProxy;
    import net.lightbody.bmp.BrowserMobProxyServer;
    import net.lightbody.bmp.client.ClientUtil;
    import net.lightbody.bmp.core.har.Har;
    import net.lightbody.bmp.proxy.CaptureType;
    import org.openqa.selenium.Proxy;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.openqa.selenium.chrome.ChromeOptions;
    import org.openqa.selenium.remote.CapabilityType;
    import org.testng.annotations.AfterMethod;
    import org.testng.annotations.BeforeMethod;
    import java.time.Duration;
    
    public class BaseTest {
        protected WebDriver driver;
        protected BrowserMobProxy proxy;
    
        @BeforeMethod
        public void setup() {
            // 1. Initialize BrowserMob Proxy
            proxy = new BrowserMobProxyServer();
            proxy.setTrustAllServers(true); // Important for HTTPS
            proxy.start(0); // Start on an ephemeral port
    
            // Set up capture types for HAR recording if needed, not strictly for mocking
            proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT, CaptureType.RESPONSE_CONTENT);
            proxy.newHar("ecommerce-test");
    
            // 2. Configure Selenium to use the proxy
            Proxy seleniumProxy = ClientUtil.create           SeleniumProxy(proxy);
            ChromeOptions options = new ChromeOptions();
            options.setCapability(CapabilityType.PROXY, seleniumProxy);
            options.setCapability(CapabilityType.ACCEPT_INSECURE_CERTS, true);
    
            // Add any other Chrome options, e.g., headless mode
            // options.addArguments("--headless");
            // options.addArguments("--window-size=1920,1080");
    
            // 3. Initialize WebDriver
            driver = new ChromeDriver(options);
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
            driver.manage().window().maximize();
        }
    
        @AfterMethod
        public void tearDown() {
            if (driver != null) {
                driver.quit();
            }
            if (proxy != null) {
                proxy.stop();
            }
        }
    }
  3. Scenario 1: Out-of-Stock Product

    We want to simulate an API response for an inventory check that shows a product is out of stock. Let’s assume the e-commerce site makes a GET request to /api/product/{productId}/inventory and expects a JSON response like {"productId": "P123", "stock": 5}. For an out-of-stock scenario, we’ll return {"productId": "P123", "stock": 0}.

    import net.lightbody.bmp.filters.ResponseFilter;
    import net.lightbody.bmp.util.HttpMessageContents;
    import net.lightbody.bmp.util.HttpMessageInfo;
    import org.openqa.selenium.By;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.support.ui.ExpectedConditions;
    import org.openqa.selenium.support.ui.WebDriverWait;
    import org.testng.Assert;
    import org.testng.annotations.Test;
    
    import java.time.Duration;
    
    public class ProductInventoryTest extends BaseTest {
    
        private static final String PRODUCT_ID = "P123"; // Example product ID
        private static final String PRODUCT_PAGE_URL = "http://localhost:8080/product/" + PRODUCT_ID; // Replace with your actual URL
        private static final By ADD_TO_CART_BUTTON = By.id("add-to-cart-button");
        private static final By INVENTORY_STATUS_TEXT = By.id("inventory-status");
    
    
        @Test(description = "Verify 'Add to Cart' is disabled for out-of-stock product")
        public void testOutOfStockProduct() {
            // Add a response filter to mock the inventory API
            proxy.addResponseFilter((response, contents, messageInfo) -> {
                if (messageInfo.getUrl().contains("/api/product/" + PRODUCT_ID + "/inventory") &&
                    messageInfo.getOriginalUrl().endsWith("/inventory")) { // Ensure it's the specific inventory endpoint
                    System.out.println("Intercepting inventory API: " + messageInfo.getUrl());
                    contents.setTextContents("{\"productId\": \"" + PRODUCT_ID + "\", \"stock\": 0, \"status\": \"OUT_OF_STOCK\"}");
                    response.setStatus(200); // Ensure a successful HTTP status
                    response.headers().set("Content-Type", "application/json");
                }
            });
    
            driver.get(PRODUCT_PAGE_URL);
    
            WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
            WebElement addToCartButton = wait.until(ExpectedConditions.presenceOfElementLocated(ADD_TO_CART_BUTTON));
    
            // Verify the 'Add to Cart' button is disabled
            Assert.assertFalse(addToCartButton.isEnabled(), "Add to Cart button should be disabled for an out-of-stock product.");
    
            // Optional: Verify specific text indicating out of stock
            WebElement statusText = wait.until(ExpectedConditions.presenceOfElementLocated(INVENTORY_STATUS_TEXT));
            Assert.assertTrue(statusText.getText().contains("Out of Stock"), "Inventory status should indicate 'Out of Stock'.");
        }
    }
  4. Scenario 2: Delayed Successful Payment

    This scenario requires a two-stage mocking: initially, return a ‘pending’ status, then after a delay, return a ‘success’ status. BrowserMob Proxy allows introducing artificial delays.

    Let’s assume the payment API endpoint is /api/checkout/pay and it returns {"status": "PENDING"} initially, followed by {"status": "SUCCESS", "transactionId": "TXN123"}.

    import net.lightbody.bmp.filters.ResponseFilter;
    import org.openqa.selenium.By;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.support.ui.ExpectedConditions;
    import org.openqa.selenium.support.ui.WebDriverWait;
    import org.testng.Assert;
    import org.testng.annotations.Test;
    
    import java.time.Duration;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    public class PaymentFlowTest extends BaseTest {
    
        private static final String CHECKOUT_PAGE_URL = "http://localhost:8080/checkout"; // Replace with your actual URL
        private static final By PLACE_ORDER_BUTTON = By.id("place-order-button");
        private static final By LOADING_INDICATOR = By.className("payment-loading-spinner");
        private static final By PAYMENT_CONFIRMATION_MESSAGE = By.id("payment-confirmation-message");
    
        @Test(description = "Verify UI handles delayed successful payment with loading state")
        public void testDelayedSuccessfulPayment() {
            AtomicBoolean firstPaymentCall = new AtomicBoolean(true);
    
            // Add a response filter to mock the payment API
            proxy.addResponseFilter((response, contents, messageInfo) -> {
                if (messageInfo.getUrl().contains("/api/checkout/pay") &&
                    messageInfo.getOriginalUrl().endsWith("/pay")) {
                    System.out.println("Intercepting payment API: " + messageInfo.getUrl());
    
                    if (firstPaymentCall.getAndSet(false)) {
                        // First call: Simulate pending state with a delay
                        proxy.addResponseFilter((res, cont, info) -> {
                            // This inner filter will run on subsequent calls to the same URL
                            // We need a mechanism to only apply delay once or change response
                            // For simplicity, directly modifying the first response and setting a future filter.
                        });
                        // Simulate a 3-second delay for the pending response
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                        contents.setTextContents("{\"status\": \"PENDING\"}");
                        response.setStatus(200);
                        response.headers().set("Content-Type", "application/json");
                        System.out.println("Payment API: Returning PENDING.");
                    } else {
                        // Subsequent call (or if the UI polls quickly): Simulate success
                        contents.setTextContents("{\"status\": \"SUCCESS\", \"transactionId\": \"TXN12345\"}");
                        response.setStatus(200);
                        response.headers().set("Content-Type", "application/json");
                        System.out.println("Payment API: Returning SUCCESS.");
                    }
                }
            });
    
            // Navigate to checkout and trigger payment
            driver.get(CHECKOUT_PAGE_URL);
            WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));
    
            WebElement placeOrderButton = wait.until(ExpectedConditions.elementToBeClickable(PLACE_ORDER_BUTTON));
            placeOrderButton.click();
    
            // Verify loading indicator appears
            wait.until(ExpectedConditions.visibilityOfElementLocated(LOADING_INDICATOR));
            System.out.println("Loading indicator displayed.");
    
            // Verify loading indicator disappears and success message appears
            wait.until(ExpectedConditions.invisibilityOfElementLocated(LOADING_INDICATOR));
            WebElement confirmationMessage = wait.until(ExpectedConditions.visibilityOfElementLocated(PAYMENT_CONFIRMATION_MESSAGE));
    
            Assert.assertTrue(confirmationMessage.getText().contains("Payment Successful"), "Payment confirmation message should be displayed.");
            Assert.assertTrue(confirmationMessage.getText().contains("TXN12345"), "Transaction ID should be present.");
            System.out.println("Payment successful message displayed and loading indicator disappeared.");
        }
    }

    Note on testDelayedSuccessfulPayment: Simulating a PENDING then SUCCESS state can be tricky with a simple ResponseFilter if the UI makes only one API call for payment and then polls another endpoint for status. If the UI polls the same endpoint, the firstPaymentCall atomic boolean logic works. If the UI makes two distinct calls (e.g., /pay then /status/{txnId}), you would need two separate filters, one for each endpoint, or a more sophisticated state machine within the filters. For simplicity, this example assumes a pattern where either the same endpoint is polled, or the delay is applied to the initial request, and the subsequent “success” is immediate in response to a later retry/poll. For a robust solution, consider a RequestFilter to modify requests and ResponseFilter to modify responses based on previous request details or a shared ThreadLocal state.

Benefits of this Approach:

  • Isolation: Each test runs with its own controlled backend state, eliminating interference between tests.
  • Speed: Avoids actual network latency and backend processing, making tests significantly faster.
  • Reliability: Eliminates external dependencies and non-determinism from fluctuating backend services.
  • Test Coverage: Enables testing of edge cases (e.g., specific error codes, unique data permutations) that are hard to reproduce with real data.
  • Early Feedback: Allows UI development and testing to proceed even if backend services are not fully ready or stable.

Considerations and Best Practices:

  • Endpoint Identification: Accurately identify the API endpoints to intercept. Regular expressions or precise URL matching might be needed.
  • Complex Scenarios: For more complex state changes (e.g., a sequence of different responses), consider implementing a more advanced state machine within your ResponseFilter logic or creating custom HarEntry objects.
  • Error Handling: Mock various error codes (e.g., 400, 500) to verify UI’s error handling.
  • Maintainability: Centralize mock data and filter logic for reusability. Consider a dedicated “MockData” class or a builder pattern for creating specific ResponseFilter instances.
  • Real Backend Integration Tests: While mocking is great for unit-like E2E tests, retain a smaller suite of high-level E2E tests that interact with real backend services to ensure full system integration.
  • SSL/HTTPS: Ensure proxy.setTrustAllServers(true) is set for HTTPS endpoints, or provide specific SSL certificates if required.
  • Proxy Port Management: The ephemeral port proxy.start(0) is robust. Ensure clean shutdown in @AfterMethod.

By adopting a proxy-based backend state simulation strategy, Staff QA Engineers can build highly effective, reliable, and maintainable Selenium test suites for complex, dynamic web applications.


📲 Practice Offline on Mobile: Download the free QA Automation & SDET Prep app on Google Play & App Store.