Page Object Model (POM)
The Page Object Model is a design pattern for UI test automation in which each web page (or significant component) of the application under test is represented by a corresponding class, whose fields are the page's locators and whose methods are the user-facing actions available on that page (login(), addToCart(), submitForm()). Test methods then call these page methods instead of embedding raw By locators and driver.findElement calls, so a test reads like a business scenario ('loginPage.login(user, pass)') while all the fragile, UI-specific detail lives in exactly one place per page.
Cricket analogy: A team's video analyst maintains one master file per opposition batsman detailing their weaknesses (e.g., 'struggles against leg-spin around off-stump') that bowlers consult rather than each bowler re-scouting from scratch; a page object is that same single, reusable reference for a specific page's UI.
Structure of a Page Object and PageFactory
A well-formed page object declares its locators as private fields (typically By objects or WebElement fields annotated with @FindBy), exposes only meaningful action and query methods publicly, and never exposes raw Selenium WebElement objects or assertions to the test layer — assertions belong in the test class, not the page object. Selenium's PageFactory.initElements(driver, this) combined with @FindBy(id = "username") annotations creates lazily-initialized proxy elements: the actual driver.findElement call only fires the moment a method is invoked on the field, which avoids StaleElementReferenceException issues that arise from locating elements too early, before the page has finished loading.
Cricket analogy: A stadium's turnstile only checks your ticket the moment you actually try to walk through, not when you first arrive at the gate; PageFactory's lazy proxy elements resolve locators only at the moment of interaction, the same just-in-time principle.
// LoginPage.java — Page Object
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class LoginPage {
private final WebDriver driver;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "loginBtn")
private WebElement loginButton;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public DashboardPage login(String username, String password) {
usernameInput.sendKeys(username);
passwordInput.sendKeys(password);
loginButton.click();
return new DashboardPage(driver); // fluent: returns next page
}
}
// LoginTest.java — Test layer, no locators, only business actions
@Test
public void validLoginNavigatesToDashboard() {
LoginPage loginPage = new LoginPage(driver);
DashboardPage dashboard = loginPage.login("qa_user", "Secr3t!");
Assert.assertTrue(dashboard.isWelcomeBannerDisplayed());
}
Assertions belong in the test class, never inside a page object method. A page object's job is only to expose actions and return state (e.g., isWelcomeBannerDisplayed() returning a boolean); keeping Assert calls out of page objects keeps them reusable across many different test scenarios that may want to assert different things about the same page.
Fluent Page Objects and Maintainability
In the fluent variant of POM, each action method returns the page object that the user lands on next (login() returns a DashboardPage, addToCart() returns the same CartPage or a Checkout page), letting tests chain calls like homePage.search("shoes").selectFirstResult().addToCart().goToCheckout() that read almost like a user story. The core payoff of POM shows up during maintenance: when a developer renames the login button's id from loginBtn to submit-login, exactly one line changes — the @FindBy in LoginPage — and every test that calls loginPage.login(...) keeps working unchanged, whereas without POM that same UI change would require hunting down and editing every test file that had the old locator hardcoded.
Cricket analogy: A well-drilled top order runs between wickets using pre-agreed calls, so batsmen chain singles into twos fluidly without confusion; fluent page objects chain page-to-page actions with that same seamless handoff.
Avoid the common anti-pattern of putting Thread.sleep() calls inside page object methods to 'wait for the page to load.' This makes tests slow and still flaky. Use explicit WebDriverWait with ExpectedConditions inside the page object's action methods instead, so waiting logic is centralized and reliable rather than scattered as arbitrary sleeps across the codebase.
- POM represents each page/component as a class encapsulating its locators and exposing user-facing action methods.
- Test classes call page object methods (e.g., loginPage.login(...)) and never contain raw By locators or findElement calls.
- PageFactory.initElements with @FindBy creates lazily-resolved proxy WebElements, reducing StaleElementReferenceException.
- Assertions belong in test classes, not inside page object methods, which should only expose actions and state queries.
- Fluent page objects return the next page object from each action method, enabling readable method-chained test scenarios.
- A single UI locator change requires editing only the page object, not every test file that interacts with that element.
- Centralize explicit waits (WebDriverWait/ExpectedConditions) inside page object methods instead of scattering Thread.sleep() calls.
Practice what you learned
1. What is the primary responsibility of a page object class in the Page Object Model?
2. What problem does PageFactory's lazy element initialization primarily help avoid?
3. In a fluent page object design, what should an action method like login() typically return?
4. Where should Assert/assertion statements live in a properly structured POM framework?
5. What is the main maintainability benefit of POM when a developer changes a button's HTML id?
Was this page helpful?
You May Also Like
Selenium with TestNG
Learn how TestNG's annotations, grouping, and reporting turn raw Selenium WebDriver scripts into a structured, parallelizable, CI-ready test suite.
Selenium with JUnit
Learn how JUnit 5's lifecycle annotations, extension model, and tagging integrate with Selenium WebDriver to build maintainable Java test suites.
Data-Driven Testing
Learn how to separate test logic from input data in Selenium frameworks using TestNG's @DataProvider and JUnit 5's @ParameterizedTest.