Implementation:SeleniumHQ Selenium Page Object Test Pattern
| Knowledge Sources | |
|---|---|
| Domains | Test_Design, Design_Patterns, Quality_Assurance |
| Last Updated | 2026-02-11 00:00 GMT |
Overview
Pattern documentation for structuring Selenium tests using Page Objects with PageFactory initialization and assertion separation.
Description
This is a Pattern Doc -- it documents a user-defined pattern, not a library API. The pattern involves: (1) creating Page Object classes with @FindBy annotations on WebElement or List<WebElement> fields, (2) initializing them with PageFactory.initElements() in the constructor, (3) writing domain-specific public methods that encapsulate element interactions, and (4) writing test methods that call Page Object methods and make assertions.
AbstractFindByBuilder validates annotations at initialization time. Its assertValidFindBy() method ensures at most one location strategy is specified per @FindBy (throwing IllegalArgumentException with a message like "You must specify at most one location strategy. Number found: N (...)"). Its assertValidFindBys() and assertValidFindAll() methods validate each @FindBy within @FindBys and @FindAll arrays respectively. If no locator attributes are set on a field, PageFactory defaults to looking up by the field name as an ID or name attribute.
Usage
Follow this pattern for all Page Object-based tests. The test class handles setup (WebDriver creation), teardown (driver.quit()), and assertions; the Page Object handles browser interaction. Use @CacheLookup for static elements to improve performance.
Code Reference
Source Location
- Repository: Selenium
- File: java/src/org/openqa/selenium/support/AbstractFindByBuilder.java (L25-121)
- File: java/src/org/openqa/selenium/support/PageFactory.java (L34-133)
- File: java/src/org/openqa/selenium/support/FindBy.java (L53-90)
- File: java/src/org/openqa/selenium/support/CacheLookup.java (L29-31)
Interface Specification
// Pattern: Page Object class structure
public class ExamplePage {
private WebDriver driver;
// 1. Annotated WebElement fields with locator strategies
@FindBy(id = "element-id")
private WebElement element;
@FindBy(css = ".static-element")
@CacheLookup
private WebElement cachedElement;
// 2. Constructor with PageFactory initialization
public ExamplePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
// 3. Domain-specific interaction methods (return Page Objects for navigation)
public ResultPage performAction(String input) {
element.clear();
element.sendKeys(input);
element.submit();
return new ResultPage(driver);
}
// 4. State query methods (for assertions in tests)
public String getDisplayedText() {
return cachedElement.getText();
}
public boolean isElementVisible() {
return element.isDisplayed();
}
}
// Pattern: Test class structure
public class ExampleTest {
private WebDriver driver;
@BeforeEach
void setUp() {
driver = new ChromeDriver();
}
@AfterEach
void tearDown() {
driver.quit();
}
@Test
void testUserJourney() {
driver.get("https://example.com");
ExamplePage page = new ExamplePage(driver);
ResultPage result = page.performAction("test input");
assertEquals("Expected result", result.getDisplayedText());
}
}
Import
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.*;
I/O Contract
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
| WebDriver | WebDriver | Yes | Active browser session; passed to Page Object constructors and PageFactory.initElements() |
| Page Object classes | Class | Yes | Classes with @FindBy-annotated fields and domain-specific methods |
| Test data | Various | Yes | Input values for Page Object methods and expected values for assertions |
Outputs
| Name | Type | Description |
|---|---|---|
| Test results | Pass/Fail | JUnit/TestNG test outcomes based on assertions |
Validation Behavior
| Validation | When | Error |
|---|---|---|
| Multiple locator strategies in single @FindBy | PageFactory.initElements() call | IllegalArgumentException: "You must specify at most one location strategy" |
| how set without using | PageFactory.initElements() call | IllegalArgumentException: "If you set the 'how' property, you must also set 'using'" |
| Class cannot be instantiated | PageFactory.initElements(context, Class) call | RuntimeException wrapping ReflectiveOperationException |
Usage Examples
Complete Test Example
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.PageFactory;
import org.junit.jupiter.api.*;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
// Page Object: SearchPage
class SearchPage {
private WebDriver driver;
@FindBy(name = "q")
private WebElement searchBox;
@FindBy(css = "button[type='submit']")
@CacheLookup
private WebElement searchButton;
public SearchPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public ResultsPage search(String query) {
searchBox.clear();
searchBox.sendKeys(query);
searchButton.click();
return new ResultsPage(driver);
}
}
// Page Object: ResultsPage
class ResultsPage {
private WebDriver driver;
@FindAll({
@FindBy(css = ".result-item"),
@FindBy(css = ".search-result")
})
private List<WebElement> results;
@FindBy(css = ".result-count")
private WebElement resultCount;
public ResultsPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public int getResultCount() {
return results.size();
}
public String getFirstResultText() {
return results.get(0).getText();
}
public String getResultCountLabel() {
return resultCount.getText();
}
}
// Test Class
class SearchTest {
private WebDriver driver;
@BeforeEach
void setUp() {
driver = new ChromeDriver();
driver.get("https://example.com/search");
}
@AfterEach
void tearDown() {
driver.quit();
}
@Test
void shouldReturnResultsForValidQuery() {
SearchPage searchPage = new SearchPage(driver);
ResultsPage results = searchPage.search("selenium");
assertTrue(results.getResultCount() > 0,
"Expected at least one search result");
}
@Test
void shouldDisplayResultCountLabel() {
SearchPage searchPage = new SearchPage(driver);
ResultsPage results = searchPage.search("webdriver");
assertNotNull(results.getResultCountLabel(),
"Expected result count label to be present");
}
}
Using PageFactory Class-Based Initialization
// PageFactory creates the instance, trying WebDriver constructor first
// then falling back to no-arg constructor
@Test
void shouldLoginWithClassBasedInit() {
LoginPage loginPage = PageFactory.initElements(driver, LoginPage.class);
DashboardPage dashboard = loginPage.login("admin", "secret");
assertEquals("Welcome, admin", dashboard.getGreeting());
}
Page Object Inheritance
// Base page with shared elements (PageFactory walks superclass hierarchy)
abstract class BasePage {
protected WebDriver driver;
@FindBy(id = "nav-menu")
@CacheLookup
protected WebElement navMenu;
@FindBy(css = ".logout-btn")
@CacheLookup
protected WebElement logoutButton;
public BasePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public LoginPage logout() {
logoutButton.click();
return new LoginPage(driver);
}
}
class DashboardPage extends BasePage {
@FindBy(css = ".greeting")
private WebElement greeting;
public DashboardPage(WebDriver driver) {
super(driver); // PageFactory initializes fields in both classes
}
public String getGreeting() {
return greeting.getText();
}
}