PracticeQAAutomation logoPracticeQAAutomation
Se

Java · TestNG

Deep dives with Selenium

8 topics that separate a script from a suite — each with a complete example that runs against this site as is.

Language

Deep dive 1Beginner

Locators that survive a redesign

Selenium gives you eight ways to find an element and no opinion about which to use. Prefer id and name, reach for XPath when you need visible text or a parent, and search inside an element instead of writing one long path.

  • ✓By.id and By.name are the most stable — use them whenever the element has one
  • ✓XPath can match visible text and choose a parent by what it contains; CSS cannot
  • ✓Searching from an element looks only inside it — start that XPath with .// or it searches the whole page

In Java: BaseTest.java starts and closes the browser and holds a few helpers; every test class in these deep dives extends it.

LocatorsTest.java
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.testng.annotations.Test;
import java.util.List;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;
import static org.testng.Assert.assertEquals;

public class LocatorsTest extends BaseTest {
  @Test
  public void findAProductTheWayAShopperDoes() {
    open("/shop");
    el(By.id("username")).sendKeys("demo_user");                          // By.id
    el(By.name("password")).sendKeys("Demo@123");                         // By.name
    el(By.xpath("//button[normalize-space()='Sign in']")).click();        // XPath on the visible text

    // One card out of twelve, found by its text — then search inside it
    WebElement lamp = wait.until(visibilityOfElementLocated(
        By.xpath("//li[@data-testid='product-card'][contains(., 'Lumen Desk Lamp')]")));
    assertEquals(lamp.findElement(tid("product-price")).getText(), "$34.99");
    lamp.findElement(By.xpath(".//button[normalize-space()='Add to cart']")).click();   // .// = inside this card
    wait.until(d -> lamp.findElement(By.xpath(".//button[normalize-space()='Remove']")).isDisplayed());

    // A parent chosen by what it contains: every card that carries a "Sale" badge
    List<WebElement> onSale = driver.findElements(By.xpath(
        "//li[@data-testid='product-card'][.//*[@data-testid='product-badge'][normalize-space()='Sale']]"));
    assertEquals(onSale.size(), 2);
  }
}
BaseTest.java
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.*;
import java.time.Duration;
import java.util.Map;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

// Shared by every test class in these deep dives: one browser per test, and a few small helpers.
public class BaseTest {
  protected static final String BASE = "https://www.practiceqaautomation.com";
  protected WebDriver driver;
  protected WebDriverWait wait;

  @BeforeMethod
  public void startBrowser() {
    ChromeOptions options = new ChromeOptions();
    // Turn off Chrome's "Save password?" pop-ups: after a login they take the keyboard away from the page.
    options.setExperimentalOption("prefs", Map.of(
        "credentials_enable_service", false,
        "profile.password_manager_enabled", false,
        "profile.password_manager_leak_detection", false));
    if (System.getenv("HEADLESS") != null) options.addArguments("--headless=new", "--window-size=1280,900");
    driver = new ChromeDriver(options);
    wait = new WebDriverWait(driver, Duration.ofSeconds(10));
  }

  @AfterMethod(alwaysRun = true)
  public void quitBrowser() {
    if (driver != null) driver.quit();
  }

  protected void open(String path) { driver.get(BASE + path); }
  protected By tid(String id) { return By.cssSelector("[data-testid='" + id + "']"); }

  /** Waits until the element can be used, scrolls it to the middle of the screen and returns it. */
  protected WebElement el(By by) {
    WebElement element = wait.until(elementToBeClickable(by));
    ((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView({block: 'center'})", element);
    return element;
  }

  protected void signInToShop(String user) {
    open("/shop");
    el(tid("username")).sendKeys(user);
    el(tid("password")).sendKeys("Demo@123");
    el(tid("login-button")).click();
    wait.until(urlMatches("/shop/(products|admin)$"));     // signed in before the test moves on
  }
}

Runs against: Nova Shop →

Deep dive 2Beginner

Explicit waits, never sleep

Selenium does not wait for anything unless you ask. An explicit wait polls for one condition and moves on the moment it is true, so the test is as fast as the page and never fails because a fixed pause was too short.

  • ✓A wait with a condition replaces every sleep, and its timeout tells you what it was waiting for
  • ✓slow_user takes about three seconds to sign in — wait for the URL, not for time
  • ✓Any function that returns true or false is a condition, so you can wait for “the prices are sorted”
WaitsTest.java
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.Select;
import org.testng.annotations.Test;
import java.util.List;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

// BaseTest (driver, wait, el, tid, open) is in the first deep dive.
public class WaitsTest extends BaseTest {
  @Test
  public void slowUserWaitForTheResultNeverForTime() {
    open("/shop");
    el(tid("username")).sendKeys("slow_user");
    el(tid("password")).sendKeys("Demo@123");
    el(tid("login-button")).click();

    wait.until(visibilityOfElementLocated(tid("login-spinner")));
    wait.until(urlContains("/shop/products"));             // polls until the slow login finishes — no Thread.sleep
    wait.until(numberOfElementsToBe(tid("product-card"), 12));
  }

  @Test
  public void waitForAConditionYouWriteYourself() {
    signInToShop("demo_user");
    new Select(el(tid("sort-select"))).selectByValue("price-asc");

    // Any lambda that returns true or false is a wait condition: here, "the prices are sorted"
    wait.until(d -> {
      List<Double> prices = d.findElements(tid("product-price")).stream()
          .map(WebElement::getText).map(t -> Double.parseDouble(t.replace("$", ""))).toList();
      return prices.size() == 12 && prices.equals(prices.stream().sorted().toList());
    });
  }
}

Runs against: Nova Shop →

Deep dive 3Intermediate

Page objects: locators in one place

When a button's id changes you want to fix it once. A page object holds one page's locators and steps, and the test calls methods that read like the user's journey.

  • ✓One class per page; its locators are private to it
  • ✓A step that leads to another page returns that page's object, so journeys chain
  • ✓Assertions stay in the test — the page object only reports what it sees
PageObjectTest.java
import org.testng.annotations.Test;
import static org.testng.Assert.*;

// The test reads like the journey; no locator appears in it. BaseTest is in the first deep dive.
public class PageObjectTest extends BaseTest {
  @Test
  public void addTwoProductsToTheCart() {
    ProductsPage products = new LoginPage(driver).open(BASE).signInAs("demo_user", "Demo@123");
    assertEquals(products.priceOf("Lumen Desk Lamp"), "$34.99");
    products.addToCart("Lumen Desk Lamp").addToCart("Aurora Wireless Headphones");
    products.waitForCartCount(2);
  }

  @Test
  public void aLockedAccountSeesAnError() {
    String message = new LoginPage(driver).open(BASE).signInExpectingError("locked_user", "Demo@123");
    assertTrue(message.toLowerCase().contains("locked"), "Unexpected message: " + message);
  }
}
LoginPage.java
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

// A page object keeps the locators and the steps for one page in one place.
public class LoginPage {
  private final WebDriver driver;
  private final WebDriverWait wait;
  private final By username = By.id("username");
  private final By password = By.id("password");
  private final By signIn = By.cssSelector("[data-testid='login-button']");
  private final By error = By.cssSelector("[data-testid='login-error']");

  public LoginPage(WebDriver driver) {
    this.driver = driver;
    this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
  }

  public LoginPage open(String baseUrl) {
    driver.get(baseUrl + "/shop");
    return this;
  }

  private void submit(String user, String pass) {
    wait.until(elementToBeClickable(username)).sendKeys(user);
    driver.findElement(password).sendKeys(pass);
    wait.until(elementToBeClickable(signIn)).click();
  }

  /** A successful login lands on the products page, so it returns that page object. */
  public ProductsPage signInAs(String user, String pass) {
    submit(user, pass);
    wait.until(urlContains("/shop/products"));
    return new ProductsPage(driver);
  }

  public String signInExpectingError(String user, String pass) {
    submit(user, pass);
    return wait.until(visibilityOfElementLocated(error)).getText();
  }
}
ProductsPage.java
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

public class ProductsPage {
  private final WebDriver driver;
  private final WebDriverWait wait;
  private final By cartBadge = By.cssSelector("[data-testid='cart-badge']");

  public ProductsPage(WebDriver driver) {
    this.driver = driver;
    this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
  }

  private By card(String productName) {
    return By.xpath("//li[@data-testid='product-card'][contains(., '" + productName + "')]");
  }

  public ProductsPage addToCart(String productName) {
    wait.until(visibilityOfElementLocated(card(productName)))
        .findElement(By.xpath(".//button[normalize-space()='Add to cart']")).click();
    return this;
  }

  public String priceOf(String productName) {
    return wait.until(visibilityOfElementLocated(card(productName)))
        .findElement(By.cssSelector("[data-testid='product-price']")).getText();
  }

  public void waitForCartCount(int count) {
    wait.until(textToBe(cartBadge, String.valueOf(count)));
  }
}

Runs against: Nova Shop →

Deep dive 4Intermediate

Sign in once, reuse it everywhere

Signing in through the form before every test is slow and tests the login a hundred times. Sign in once, keep what the site stored, and put it back at the start of each test.

  • ✓Nova Shop keeps its session in localStorage, so one script call saves it and one restores it
  • ✓A page of the site must be open before you can write its storage
  • ✓Sites that use cookies work the same way with the driver's cookie methods
SessionTest.java
import org.openqa.selenium.JavascriptExecutor;
import org.testng.annotations.*;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

// BaseTest (driver, wait, el, tid, open, signInToShop) is in the first deep dive.
public class SessionTest extends BaseTest {
  static String savedSession;   // what Nova Shop keeps in localStorage after a login

  // Sign in through the UI once, and keep the saved session
  @BeforeClass
  public void signInOnce() {
    startBrowser();
    signInToShop("demo_user");
    savedSession = (String) ((JavascriptExecutor) driver).executeScript("return localStorage.getItem('pa-shop')");
    quitBrowser();
  }

  // Every test starts signed in without touching the login form
  @BeforeMethod(dependsOnMethods = "startBrowser")
  public void restoreSession() {
    open("/shop");   // a page of the site must be open before its localStorage can be written
    ((JavascriptExecutor) driver).executeScript("localStorage.setItem('pa-shop', arguments[0])", savedSession);
  }

  @Test
  public void startsOnTheProductsPageAlreadySignedIn() {
    open("/shop/products");                                // no login step
    wait.until(textToBePresentInElementLocated(tid("user-greeting"), "demo_user"));
    wait.until(numberOfElementsToBe(tid("product-card"), 12));
  }

  @Test
  public void isStillSignedInForTheNextTest() {
    open("/shop/products");
    el(tid("add-to-cart-p1")).click();
    wait.until(textToBe(tid("cart-badge"), "1"));
  }
}

Runs against: Nova Shop →

Deep dive 5Advanced

Control the network

Empty lists and server errors are hard to produce on a real server. Chrome can run a script before the page's own code, which lets you replace window.fetch and answer a request yourself.

  • ✓Page.addScriptToEvaluateOnNewDocument, a Chrome DevTools command, runs your script before every page load
  • ✓The stub answers only the request you name and passes everything else to the real fetch
  • ✓It uses the DevTools protocol, so it works in Chrome and Edge only
NetworkTest.java
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chromium.ChromiumDriver;
import org.testng.annotations.Test;
import java.util.Map;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;
import static org.testng.Assert.assertEquals;

// BaseTest (driver, wait, el, tid, open) is in the first deep dive.
public class NetworkTest extends BaseTest {
  /** Runs a script in every new page before the page's own code — the place to replace window.fetch. */
  void beforeEveryPage(String script) {
    ((ChromiumDriver) driver).executeCdpCommand("Page.addScriptToEvaluateOnNewDocument", Map.of("source", script));
  }

  @Test
  public void emptyListShowsTheEmptyState() {
    beforeEveryPage("""
        const realFetch = window.fetch;
        window.fetch = (input, init) => String(input).includes('/mock/products.json')
          ? Promise.resolve(new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } }))
          : realFetch(input, init);
        """);
    open("/dynamic/network-mocking");
    wait.until(textToBe(tid("net-empty"), "No products found."));
    assertEquals(driver.findElements(tid("net-item")).size(), 0);
  }

  @Test
  public void aServerErrorCanBeRetried() {
    beforeEveryPage("""
        const realFetch = window.fetch;
        window.productCalls = 0;
        window.fetch = (input, init) => {
          if (String(input).includes('/mock/products.json') && ++window.productCalls === 1) {
            return Promise.resolve(new Response('Internal Server Error', { status: 500 }));
          }
          return realFetch(input, init);                   // the retry reaches the real server
        };
        """);
    open("/dynamic/network-mocking");
    wait.until(textToBePresentInElementLocated(By.cssSelector("[data-testid='net-app'] [role='alert']"), "(500)"));
    el(By.xpath("//button[normalize-space()='Try again']")).click();
    wait.until(numberOfElementsToBe(tid("net-item"), 8));
    assertEquals(((JavascriptExecutor) driver).executeScript("return window.productCalls"), 2L);
  }
}

Runs against: Network mocking →

Deep dive 6Advanced

Check the UI against the API

A UI test proves the screen shows a number. A hybrid test proves it is the right number. Selenium only drives the browser, so the API call uses the language's own HTTP client.

  • ✓Call the API with the HTTP client your language already has — no extra tool needed
  • ✓Nova Shop's quote API uses the same pricing rules as the cart screen
  • ✓Compare the two instead of hard-coding totals that break when a price changes
HybridTest.java
import org.openqa.selenium.json.Json;
import org.testng.annotations.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Locale;
import java.util.Map;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;
import static org.testng.Assert.*;

// Selenium drives the browser; the API call is plain Java (java.net.http). BaseTest is in the first deep dive.
public class HybridTest extends BaseTest {
  HttpResponse<String> quote(String json) throws Exception {
    HttpRequest request = HttpRequest.newBuilder(URI.create(BASE + "/api/shop/quote"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json)).build();
    return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
  }

  static String usd(Map<String, Object> quote, String field) {
    return String.format(Locale.US, "$%.2f", ((Number) quote.get(field)).doubleValue());
  }

  @Test
  public void cartTotalsInTheUiMatchTheQuoteApi() throws Exception {
    signInToShop("demo_user");
    el(tid("add-to-cart-p1")).click();
    el(tid("add-to-cart-p4")).click();
    el(tid("cart-link")).click();
    el(tid("coupon-input")).sendKeys("SAVE10");
    el(tid("apply-coupon")).click();

    HttpResponse<String> res = quote("""
        {"items": [{"id": "p1", "qty": 1}, {"id": "p4", "qty": 1}], "coupon": "SAVE10"}""");
    assertEquals(res.statusCode(), 200);
    Map<String, Object> quote = new Json().toType(res.body(), Json.MAP_TYPE);

    wait.until(textToBe(tid("subtotal"), usd(quote, "subtotal")));
    wait.until(textToBe(tid("discount"), "−" + usd(quote, "discount")));
    wait.until(textToBe(tid("tax"), usd(quote, "tax")));
    wait.until(textToBe(tid("total"), usd(quote, "total")));
  }

  @Test
  public void anUnknownCouponIsRejectedByTheApiToo() throws Exception {
    HttpResponse<String> res = quote("""
        {"items": [{"id": "p1", "qty": 1}], "coupon": "ABC123"}""");
    assertEquals(res.statusCode(), 422);
    Map<String, Object> body = new Json().toType(res.body(), Json.MAP_TYPE);
    assertTrue(body.get("error").toString().contains("not a valid coupon"));
  }
}

Runs against: Shop & bank APIs →

Deep dive 7Advanced

Two roles in one test

Real workflows cross users: a customer orders, a manager ships. One test can follow the whole journey if a small helper switches user and the order id is carried from step to step.

  • ✓A sign-in helper switches role without repeating the login steps
  • ✓Read the order id from the page and reuse it — never hard-code generated data
  • ✓The second role's change must be visible to the first role afterwards
RolesTest.java
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.Select;
import org.testng.annotations.Test;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

// BaseTest (driver, wait, el, tid, open, signInToShop) is in the first deep dive.
public class RolesTest extends BaseTest {
  By rowFor(String rowTestId, String orderId) {
    return By.xpath("//tr[@data-testid='" + rowTestId + "'][contains(., '" + orderId + "')]");
  }

  @Test
  public void customerOrdersManagerShipsCustomerSeesShipped() {
    // 1. The customer places an order
    signInToShop("demo_user");
    el(tid("add-to-cart-p4")).click();
    el(tid("cart-link")).click();
    el(tid("checkout-button")).click();
    el(tid("first-name")).sendKeys("Asha");
    el(tid("last-name")).sendKeys("Rao");
    el(tid("email")).sendKeys("asha@example.com");
    el(tid("phone")).sendKeys("5551234567");
    el(tid("address")).sendKeys("12 Lake Road");
    el(tid("city")).sendKeys("Austin");
    new Select(el(tid("state"))).selectByVisibleText("Texas");
    el(tid("zip")).sendKeys("73301");
    el(tid("payment-cod")).click();
    el(tid("accept-terms")).click();
    el(tid("place-order")).click();
    String orderId = wait.until(visibilityOfElementLocated(tid("order-id"))).getText();   // read it, never hard-code it
    el(tid("logout-button")).click();

    // 2. The store manager ships it
    signInToShop("admin_user");
    wait.until(urlContains("/shop/admin"));
    WebElement adminRow = wait.until(visibilityOfElementLocated(rowFor("admin-order-row", orderId)));
    wait.until(textToBePresentInElement(adminRow.findElement(tid("admin-order-customer")), "demo_user"));
    adminRow.findElement(By.xpath(".//button[normalize-space()='Mark shipped']")).click();
    wait.until(textToBe(By.xpath("//tr[@data-order-id='" + orderId + "']//*[@data-testid='admin-order-status']"), "Shipped"));
    el(tid("logout-button")).click();

    // 3. The customer sees the new status
    signInToShop("demo_user");
    open("/shop/orders");
    wait.until(textToBe(By.xpath("//tr[@data-order-id='" + orderId + "']//*[@data-testid='order-status']"), "Shipped"));
  }

  @Test
  public void aCustomerCannotOpenTheAdminPage() {
    signInToShop("demo_user");
    open("/shop/admin");
    wait.until(visibilityOfElementLocated(tid("access-denied")));
  }
}

Runs against: Nova Shop admin →

Deep dive 8Intermediate

Evidence when a test fails

When a test fails in CI you can't watch it. A screenshot, the URL and the browser console, saved at the moment of failure, turn “it failed” into something you can read.

  • ✓Take the screenshot in a hook that runs before the browser closes
  • ✓The driver's logs return what the page printed to the console
  • ✓HEADLESS=1 runs without a window — the same switch your CI uses
EvidenceOnFailureTest.java
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.logging.LogType;
import org.testng.ITestResult;
import org.testng.annotations.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import static org.openqa.selenium.support.ui.ExpectedConditions.titleContains;

// Run without a window:  HEADLESS=1 mvn test   (BaseTest, in the first deep dive, reads HEADLESS)
public class EvidenceOnFailureTest extends BaseTest {
  // A subclass's @AfterMethod runs before BaseTest closes the browser, so the page is still there.
  @AfterMethod(alwaysRun = true)
  public void keepEvidence(ITestResult result) throws IOException {
    if (result.getStatus() != ITestResult.FAILURE) return;

    File shot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    Path target = Paths.get("screenshots", result.getName() + ".png");
    Files.createDirectories(target.getParent());
    Files.copy(shot.toPath(), target, StandardCopyOption.REPLACE_EXISTING);

    System.out.println("Failed at: " + driver.getCurrentUrl());
    driver.manage().logs().get(LogType.BROWSER)            // what the browser console printed
        .forEach(entry -> System.out.println(entry.getLevel() + " " + entry.getMessage()));
  }

  @Test
  public void theHomePageLoads() {
    open("/");
    wait.until(titleContains("PracticeQAAutomation"));
  }
}

Runs against: Framework & CI →

Practicing another tool?