Home / Blog / Page Object Model in Selenium: Design and Best Practices

Selenium

Page Object Model in Selenium: Design and Best Practices

QA Knowledge Hub·2026-04-17·8 min read

Page Object Model (POM) is the most important design pattern in test automation. Every SDET interview asks about it. Understanding it — not just reciting its definition — is what separates junior from mid-level automation engineers.

Why POM Exists

Without POM, automation code looks like this:

// Test 1 — Login test
driver.findElement(By.id("email")).sendKeys("user@test.com");
driver.findElement(By.id("password")).sendKeys("pass123");
driver.findElement(By.id("login-btn")).click();

// Test 2 — Profile test (also needs to log in first)
driver.findElement(By.id("email")).sendKeys("user@test.com");
driver.findElement(By.id("password")).sendKeys("pass123");
driver.findElement(By.id("login-btn")).click();  // Duplicated!
driver.findElement(By.id("profile-link")).click();

// Test 3 — Checkout test (also needs to log in first)
driver.findElement(By.id("email")).sendKeys("user@test.com");
driver.findElement(By.id("password")).sendKeys("pass123");
driver.findElement(By.id("login-btn")).click();  // Duplicated again!

Now the developer changes id="login-btn" to id="signin-button". You must find and update every occurrence across all test files.

With 50 tests in a real suite, this becomes unmanageable.

What POM Solves

POM separates where the locators and interactions live (Page Object classes) from what the tests verify (test classes).

Each page of the application gets its own class. The class owns all locators for that page and all methods to interact with it. Tests call methods — they never touch locators directly.

When the login button ID changes, you update the locator in one place — LoginPage.java. All 50 tests that call loginPage.login(email, password) continue working without any changes.

The Core Structure

src/main/java/com/qaknowledgehub/
├── pages/
│   ├── BasePage.java       ← Shared WebDriver helpers
│   ├── LoginPage.java      ← Login page interactions
│   ├── DashboardPage.java  ← Dashboard interactions
│   └── CheckoutPage.java   ← Checkout interactions
└── utils/
    └── DriverFactory.java

src/test/java/com/qaknowledgehub/
└── tests/
    ├── LoginTest.java
    ├── DashboardTest.java
    └── CheckoutTest.java

BasePage — Shared Interaction Helpers

package com.qaknowledgehub.pages;

import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;

public abstract class BasePage {

    protected final WebDriver driver;
    protected final WebDriverWait wait;

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

    protected void click(By locator) {
        wait.until(ExpectedConditions.elementToBeClickable(locator)).click();
    }

    protected void type(By locator, String text) {
        WebElement element = wait.until(
            ExpectedConditions.visibilityOfElementLocated(locator)
        );
        element.clear();
        element.sendKeys(text);
    }

    protected String getText(By locator) {
        return wait.until(
            ExpectedConditions.visibilityOfElementLocated(locator)
        ).getText();
    }

    protected boolean isVisible(By locator) {
        try {
            return wait.until(
                ExpectedConditions.visibilityOfElementLocated(locator)
            ).isDisplayed();
        } catch (TimeoutException | NoSuchElementException e) {
            return false;
        }
    }

    protected void waitForUrl(String urlContains) {
        wait.until(ExpectedConditions.urlContains(urlContains));
    }

    protected boolean isOnPage(String urlFragment) {
        return driver.getCurrentUrl().contains(urlFragment);
    }
}

Making BasePage abstract prevents instantiation — you can only create subclasses.

LoginPage — A Complete Page Object

package com.qaknowledgehub.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class LoginPage extends BasePage {

    // All locators in one place — private, static, final
    private static final By EMAIL_FIELD    = By.id("user-name");
    private static final By PASSWORD_FIELD = By.id("password");
    private static final By LOGIN_BUTTON   = By.id("login-button");
    private static final By ERROR_MESSAGE  = By.cssSelector("[data-test='error']");

    public LoginPage(WebDriver driver) {
        super(driver);
    }

    // Navigation
    public LoginPage open() {
        driver.get("https://www.saucedemo.com");
        return this;
    }

    // Actions
    public DashboardPage loginAs(String username, String password) {
        type(EMAIL_FIELD, username);
        type(PASSWORD_FIELD, password);
        click(LOGIN_BUTTON);
        return new DashboardPage(driver);  // Returns the NEXT page
    }

    public LoginPage submitInvalidLogin(String username, String password) {
        type(EMAIL_FIELD, username);
        type(PASSWORD_FIELD, password);
        click(LOGIN_BUTTON);
        return this;  // Stays on login page if login fails
    }

    // State queries
    public boolean isErrorVisible() {
        return isVisible(ERROR_MESSAGE);
    }

    public String getErrorMessage() {
        return getText(ERROR_MESSAGE);
    }
}

Notice that loginAs() returns a DashboardPage — this is the Page Chain pattern. After a successful login, you get back the next page object. This makes test code read naturally.

DashboardPage

package com.qaknowledgehub.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import java.util.List;
import java.util.stream.Collectors;

public class DashboardPage extends BasePage {

    private static final By PAGE_TITLE     = By.cssSelector(".title");
    private static final By PRODUCT_ITEMS  = By.cssSelector(".inventory_item");
    private static final By PRODUCT_NAMES  = By.cssSelector(".inventory_item_name");
    private static final By SORT_DROPDOWN  = By.cssSelector("[data-test='product-sort-container']");
    private static final By CART_BUTTON    = By.cssSelector(".shopping_cart_link");
    private static final By CART_BADGE     = By.cssSelector(".shopping_cart_badge");

    public DashboardPage(WebDriver driver) {
        super(driver);
        waitForUrl("/inventory");  // Ensure we're on the right page
    }

    public boolean isLoaded() {
        return isOnPage("/inventory");
    }

    public String getPageTitle() {
        return getText(PAGE_TITLE);
    }

    public int getProductCount() {
        return driver.findElements(PRODUCT_ITEMS).size();
    }

    public List<String> getProductNames() {
        return driver.findElements(PRODUCT_NAMES)
            .stream()
            .map(e -> e.getText())
            .collect(Collectors.toList());
    }

    public DashboardPage addFirstProductToCart() {
        driver.findElements(By.cssSelector(".btn_inventory")).get(0).click();
        return this;
    }

    public int getCartItemCount() {
        if (!isVisible(CART_BADGE)) return 0;
        return Integer.parseInt(getText(CART_BADGE));
    }

    public CartPage openCart() {
        click(CART_BUTTON);
        return new CartPage(driver);
    }
}

Test Classes — Clean and Readable

package com.qaknowledgehub.tests;

import com.qaknowledgehub.base.BaseTest;
import com.qaknowledgehub.pages.DashboardPage;
import com.qaknowledgehub.pages.LoginPage;
import org.testng.Assert;
import org.testng.annotations.Test;

public class LoginTest extends BaseTest {

    @Test(description = "Valid login redirects to inventory dashboard")
    public void testSuccessfulLogin() {
        DashboardPage dashboard = new LoginPage(driver)
            .open()
            .loginAs("standard_user", "secret_sauce");

        Assert.assertTrue(dashboard.isLoaded(), "Should be on dashboard after login");
        Assert.assertEquals(dashboard.getPageTitle(), "Products");
    }

    @Test(description = "Locked out user sees error message")
    public void testLockedOutUser() {
        LoginPage loginPage = new LoginPage(driver)
            .open()
            .submitInvalidLogin("locked_out_user", "secret_sauce");

        Assert.assertTrue(loginPage.isErrorVisible());
        Assert.assertTrue(loginPage.getErrorMessage().contains("locked out"));
    }

    @Test(description = "Login with empty credentials shows validation error")
    public void testEmptyCredentials() {
        LoginPage loginPage = new LoginPage(driver).open();
        loginPage.submitInvalidLogin("", "");

        Assert.assertTrue(loginPage.isErrorVisible());
        Assert.assertTrue(loginPage.getErrorMessage().contains("Username is required"));
    }
}

Compare this to the non-POM version at the start. The test is readable as a sentence: "Open login page, log in as standard_user, assert we are on the dashboard." No WebDriver calls, no locators, no implementation details.

POM with PageFactory

An alternative implementation using Selenium's PageFactory:

package com.qaknowledgehub.pages;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class LoginPageFactory {

    @FindBy(id = "user-name")
    private WebElement usernameField;

    @FindBy(id = "password")
    private WebElement passwordField;

    @FindBy(id = "login-button")
    private WebElement loginButton;

    @FindBy(css = "[data-test='error']")
    private WebElement errorMessage;

    private final WebDriver driver;

    public LoginPageFactory(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public void login(String username, String password) {
        usernameField.clear();
        usernameField.sendKeys(username);
        passwordField.sendKeys(password);
        loginButton.click();
    }

    public String getErrorText() {
        return errorMessage.getText();
    }
}

When to use PageFactory vs By locators:

  • PageFactory: Slightly cleaner annotation syntax; elements initialised lazily on first access
  • By locators: More explicit; easier to wrap in explicit waits; preferred when using BasePage helper methods

Neither is wrong. Choose one and be consistent across the project.

Advanced POM Patterns

Fluent Page Object Interface

// Method chaining for readability
DashboardPage dashboard = new LoginPage(driver)
    .open()
    .loginAs("standard_user", "secret_sauce");

dashboard
    .addFirstProductToCart()
    .addFirstProductToCart()
    .openCart()
    .proceedToCheckout()
    .fillShippingInfo("Test", "User", "12345")
    .finishCheckout();

Each method returns the appropriate page object, making tests read like a user story.

Component Objects for Reusable UI Parts

Some UI components appear on multiple pages (headers, navigation, sidebars). Extract them as Component Objects:

// Navigation component appears on every authenticated page
public class NavBar {
    private final WebDriver driver;
    private static final By CART_LINK  = By.cssSelector(".shopping_cart_link");
    private static final By MENU_BTN   = By.id("react-burger-menu-btn");
    private static final By LOGOUT_BTN = By.id("logout_sidebar_link");

    public NavBar(WebDriver driver) {
        this.driver = driver;
    }

    public void openMenu() {
        driver.findElement(MENU_BTN).click();
    }

    public void logout() {
        openMenu();
        new WebDriverWait(driver, Duration.ofSeconds(5))
            .until(ExpectedConditions.elementToBeClickable(LOGOUT_BTN))
            .click();
    }
}

// Used inside any page that has a navbar
public class DashboardPage extends BasePage {
    public final NavBar navbar;

    public DashboardPage(WebDriver driver) {
        super(driver);
        this.navbar = new NavBar(driver);  // Composed, not inherited
    }
}

POM Interview Questions

Q: What is the Page Object Model?

POM is a design pattern that creates a class for each page or component in the application. Each class contains: (1) element locators as private constants and (2) public methods for interacting with those elements. Test classes call methods on page objects — they never interact with WebDriver directly. This separates what is being tested from how the UI is navigated, making tests more readable and making locator updates cheaper (one change in one place vs. changes across all tests).


Q: What is the difference between POM and PageFactory?

POM is a design pattern (a concept). PageFactory is a Selenium utility class that provides @FindBy annotations and initElements() to initialise page elements. You can implement POM with By locators (the manual approach) or with PageFactory annotations. Both are correct. PageFactory is a convenience tool, not a requirement for POM.


Q: What are the advantages of POM?

  1. Maintainability: UI changes only require updating the Page Object, not every test
  2. Readability: Tests describe user scenarios, not implementation details
  3. Reusability: Common interactions (login, navigation) are written once and reused everywhere
  4. Separation of concerns: Test logic is separate from page interaction logic

Q: What is a disadvantage of POM?

POM adds overhead — more classes, more files. For small suites (under 20 tests), it may be premature. It also requires discipline: developers must resist putting assertions in page objects (assertions belong in tests) and business logic in page objects (which should only handle UI interactions).


Q: Can you put assertions in a Page Object?

This is debated. The strict answer: no. Assertions are test logic and belong in test classes. Page objects should only provide state-query methods (isErrorVisible(), getPageTitle()) that tests use in assertions. Putting assertions in page objects makes them untestable and harder to reuse across different assertion contexts.

Summary

Page Object Model exists to solve a specific problem: locator duplication and maintenance cost in test automation. Every time you find yourself writing the same locator in multiple test files, POM is the answer.

The pattern is not complicated. One class per page. Locators private, methods public. Tests call methods, not WebDriver. The details — PageFactory vs By, component objects, fluent interfaces — are refinements on top of this simple principle.

Know this pattern cold. Implement it in your portfolio project. Every SDET interview will ask about it, and a working implementation is worth more than any definition.

Recommended Resource

QA Interview Kit

Interview prep kit with real-world QA and API scenarios.

999Get This Guide →

Related Posts

📝
Interview Prep
Apr 2026·11 min read

60 Selenium Interview Questions for Java Developers

The most asked Selenium + Java interview questions with complete answers — covering WebDriver setup, locators, waits, POM, TestNG, and framework design.

Read article →
📝
Web Scraping
Apr 2026·7 min read

Web Scraping with Selenium and Python: Beginner to Intermediate

Learn how to scrape websites using Selenium and Python — handle dynamic content, pagination, authentication, and export data to CSV or JSON.

Read article →
📝
Mobile Testing
Apr 2026·8 min read

Appium Mobile Testing: Android Setup from Scratch

Set up Appium for Android testing — install prerequisites, configure emulator, write your first test in Java, and understand locator strategies for mobile.

Read article →