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.
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.
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);
}
}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
}
}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”
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());
});
}
}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
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);
}
}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();
}
}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)));
}
}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
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"));
}
}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
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);
}
}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
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"));
}
}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
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")));
}
}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
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"));
}
}