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:
- Inventory Levels: An item out of stock should disable âAdd to Cartâ. Low stock might show a warning.
- Promotional Eligibility: Discounts apply only when specific criteria are met (e.g., cart value, user group), altering displayed prices.
- 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:
- 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.
- 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:
- Start and Stop a Proxy Server: Managed programmatically within test setup and teardown.
- Capture Network Traffic: Monitor all requests and responses.
- Manipulate Requests/Responses: Dynamically modify headers, status codes, and body content of specific API calls.
- 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:
-
Add Dependencies: Include
browsermob-core,selenium-java, and a test framework like TestNG or JUnit in yourpom.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> -
Basic Proxy and WebDriver Setup: A base test class
BaseTestwill 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(); } } } -
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}/inventoryand 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'."); } } -
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/payand 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 aPENDINGthenSUCCESSstate can be tricky with a simpleResponseFilterif the UI makes only one API call for payment and then polls another endpoint for status. If the UI polls the same endpoint, thefirstPaymentCallatomic boolean logic works. If the UI makes two distinct calls (e.g.,/paythen/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 aRequestFilterto modify requests andResponseFilterto modify responses based on previous request details or a sharedThreadLocalstate.
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
ResponseFilterlogic or creating customHarEntryobjects. - 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
ResponseFilterinstances. - 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.