Every Selenium test boils down to find an element, do something with it, assert something about it. Lessons 3 and 4 in chapter 1 already used By.id and By.linkText. This chapter is about the full toolkit — every By strategy Selenium ships, when each one earns its place, and how to rank them so future-you isn't fixing flaky locators every Friday afternoon. By the end of this lesson you'll know the seven By factories, the rule about findElement vs findElements, and which strategy to reach for first when you open DevTools on a new page.
The seven By strategies
Every Selenium locator goes through the By class. There are seven factory methods, and you'll use three of them 90% of the time:
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
// 1. By ID — fastest, most reliable, when stable IDs exist
WebElement email = driver.findElement(By.id("email-input"));
// 2. By name attribute — common on forms
WebElement password = driver.findElement(By.name("password"));
// 3. By class name — a single CSS class (no spaces)
WebElement submit = driver.findElement(By.className("submit-button"));
// 4. By tag name — rarely useful alone, returns way too many matches
List<WebElement> allButtons = driver.findElements(By.tagName("button"));
// 5. By link text — the visible text of an <a> element
WebElement signIn = driver.findElement(By.linkText("Sign In"));
// 6. By partial link text — substring match on <a> text
WebElement signInPartial = driver.findElement(By.partialLinkText("Sign"));
// 7. By CSS selector — powerful, concise, fast
WebElement byTestId = driver.findElement(By.cssSelector("[data-testid='submit']"));
WebElement nested = driver.findElement(By.cssSelector("form#login button[type='submit']"));
// Bonus: By XPath — most flexible, slowest, can navigate DOM tree
WebElement byXpath = driver.findElement(By.xpath("//button[@type='submit']"));
WebElement priceInCard = driver.findElement(
By.xpath("//div[@class='product-card']//span[@class='price']")
);Behind the scenes, Selenium translates By.id("foo") into document.getElementById("foo"), and By.cssSelector(...) into document.querySelector(...). The browser's DOM engine does the actual finding. That's why CSS and XPath are slower than ID — the engine has to parse and execute a query rather than do a single hash lookup.
findElement vs findElements
Two methods, one critical distinction:
// findElement — returns ONE WebElement, throws NoSuchElementException if none match
WebElement card = driver.findElement(By.cssSelector(".product-card"));
// findElements — returns a List<WebElement>, returns empty list if none match
List<WebElement> cards = driver.findElements(By.cssSelector(".product-card"));
System.out.println("Found " + cards.size() + " product cards");
if (cards.isEmpty()) {
System.out.println("No products on this page yet");
}The findElements (plural) form is what you reach for when:
- You want to count how many of something exists (
cards.size()) - You want to iterate over a collection (every product, every row)
- You want to assert "no error message exists" without an exception
If you call findElement (singular) and the element doesn't exist, Selenium throws NoSuchElementException. If you don't catch it, the test fails. That's usually the right behaviour — but for "is this element absent?" assertions, use findElements(...).isEmpty() instead.
The locator priority order
Not all locators are created equal. When you have multiple options, prefer the one higher up:
Locator strategy ranking — reliability and speed
Two refinements to the chart:
By.idonly beatsdata-testidwhen the ID is genuinely stable. Modern frameworks generate IDs that look likeid="aria-12345"and change every render. Don't chase those. Adata-testidon the same element is more durable.By.xpathranks below CSS for most cases, but jumps to first place when you need to anchor on visible text or walk up the DOM tree (a parent of a known element). Lesson 2 covers exactly when XPath earns its keep.
When to use CSS vs XPath
Two tools, mostly overlapping:
| Need | CSS | XPath |
|---|---|---|
| Match by attribute | [data-testid='foo'] | [@data-testid='foo'] |
| Descendant | .card .price | //div[@class='card']//span[@class='price'] |
| Direct child | .nav > li | /li |
| Nth child | :nth-child(3) | [3] |
| By visible text | ❌ not supported | [text()='Submit'] |
| Walk to parent | ❌ not supported | parent::div, .. |
| Sibling (next) | + | following-sibling:: |
| Sibling (previous) | ❌ not supported | preceding-sibling:: |
| Performance | faster (browser-native) | slightly slower (interpreted) |
The summary: use CSS by default. Reach for XPath when you need text matching (text()='Submit'), parent traversal (/parent::tr), or previous-sibling selection. Lessons 2 and 3 of this chapter dive deep into each.
Selenium 4 — relative locators
Selenium 4 added a feature for selectors expressed in terms of position relative to another element:
import static org.openqa.selenium.support.locators.RelativeLocator.with;
// "The input ABOVE the password field"
WebElement emailField = driver.findElement(
with(By.tagName("input")).above(By.id("password"))
);
// "The button NEAR the email field" (within ~50 pixels)
WebElement helpButton = driver.findElement(
with(By.tagName("button")).near(By.id("email"))
);
// Other directions: below, toLeftOf, toRightOfHonest assessment: relative locators sound elegant but rely on the rendered geometry, which is the most fragile thing on the page. A CSS change that adds 12 pixels of margin can break them. Treat them as a tool of last resort, not a default.
How this differs from Cypress and Playwright
If you've used Cypress or Playwright, the locator API in those frameworks is more user-centric:
// Playwright
page.getByRole("button", { name: "Sign in" });
page.getByLabel("Email address");
page.getByPlaceholder("you@example.com");
page.getByTestId("submit");
// Cypress
cy.get("[data-testid='submit']");
cy.contains("button", "Sign in");These are deliberately closer to what the user sees (a button with the visible text "Sign in", an input with the label "Email address"). Selenium's By family is more DOM-centric (element type, attribute, hierarchy). Selenium gives you more raw control; the modern frameworks give you more abstraction.
The bridge is the data-testid pattern: By.cssSelector("[data-testid='submit']") is exactly as readable as Playwright's getByTestId("submit"). Use it consistently and Selenium feels much closer to the modern frameworks.
A real product-listing example
A test that exercises four locator strategies on a single Sauce Demo–style page:
package com.mycompany.tests.tests;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.List;
public class LocatorStrategiesTest {
WebDriver driver;
@BeforeMethod
public void setup() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.get("https://www.saucedemo.com");
// Log in via By.id and By.name (Sauce Demo uses both)
driver.findElement(By.id("user-name")).sendKeys("standard_user");
driver.findElement(By.id("password")).sendKeys("secret_sauce");
driver.findElement(By.cssSelector("[data-test='login-button']")).click();
}
@Test
public void shouldShowSixInventoryItems() {
// findElements — returns a List, asserts on count
List<WebElement> cards = driver.findElements(By.cssSelector("[data-test='inventory-item']"));
Assert.assertEquals(cards.size(), 6, "Sauce Demo always shows six products");
}
@Test
public void shouldHaveSauceLabsBackpackProduct() {
// XPath because we need text-content match
WebElement backpack = driver.findElement(
By.xpath("//div[@class='inventory_item_name ' and text()='Sauce Labs Backpack']")
);
Assert.assertTrue(backpack.isDisplayed());
}
@Test
public void shouldHaveCartIconInHeader() {
// CSS for nested attribute selection
WebElement cart = driver.findElement(
By.cssSelector("#shopping_cart_container .shopping_cart_link")
);
Assert.assertTrue(cart.isDisplayed());
}
@AfterMethod
public void teardown() {
if (driver != null) driver.quit();
}
}Three tests, four strategies. Notice that data-test (Sauce Demo's chosen attribute name — same idea as data-testid) wins for the login button; CSS wins for the nested header lookup; XPath earns its place when we need to match by visible text.
The full reference for every method covered here lives on the Selenium tool entry and the XPath & CSS selectors cheat sheet.
⚠️ Common mistakes
- Reaching for XPath by reflex. A surprising number of Selenium codebases are 90% XPath, 10% everything else. CSS is faster, more readable, and just as expressive for most cases. Default to CSS; pick XPath only when you need text matching or parent traversal.
- Using
findElementto check if something is absent.driver.findElement(By.id("error"))throwsNoSuchElementExceptionif no error is present — and the exception isn't an "absence assertion," it's a test-runner failure. Usedriver.findElements(By.id("error")).isEmpty()to express "no error exists" cleanly. - Anchoring on hashed CSS classes.
class="css-1a2b3c"from a CSS-in-JS library changes every build. Tests that target it pass once and break the next morning. Either co-designdata-testidattributes with the dev team or anchor on more stable selectors (a wrapping ID, anaria-label, the section's role).
🎯 Practice task
Use the full locator toolkit on Sauce Demo. 30–40 minutes.
- Create
LocatorStrategiesTestfrom this lesson undersrc/test/java/com/mycompany/tests/tests/. Run it. All three tests should pass. - Add a fourth test,
shouldHaveSortDropdown, that locates the sort dropdown usingBy.cssSelector("[data-test='product-sort-container']")and asserts it's displayed. - Compare CSS vs XPath for the same element. Find the "Add to cart" button on the Sauce Labs Backpack card two ways:
- CSS:
By.cssSelector("[data-test='add-to-cart-sauce-labs-backpack']") - XPath:
By.xpath("//button[@id='add-to-cart-sauce-labs-backpack']")Write both as separate test methods and confirm both pass. Note which one is shorter and easier to read.
- CSS:
- Force a
NoSuchElementException. Add a test that doesdriver.findElement(By.id("does-not-exist")). Run it. Read the failure message and stack trace — that's what every locator typo looks like. Replace it withdriver.findElements(By.id("does-not-exist")).isEmpty()and assert it returns true. - Stretch — relative locators. Find the "Username" label, then use
with(By.tagName("input")).below(By.id("user-name-label"))to locate the input below it. Compare withBy.id("user-name"). The relative locator works but is more fragile — appreciate the trade-off without committing to use them.
Next lesson: XPath in depth. We'll cover the axes (parent::, following-sibling::, preceding-sibling::), text-content matching, and the patterns you'll lean on when CSS just can't reach the element you need.