Page Object Model Selenium

The Page Object Model (POM) is the design pattern behind almost every maintainable Selenium framework: each page gets a class that knows its locators and actions, and tests talk to pages instead of raw elements. This guide explains framework types, then builds a page object two ways — with plain By locators and with PageFactory — with working code, and covers the mistakes that cause the most trouble.

What Is a Test Automation Framework?

A framework is the structure around your tests: how the browser starts, where locators and test data live, how tests run and report. It gives a team consistency, reuse and maintainability — adding the 500th test should be as easy as the 5th. The full end-to-end design is in The Complete Automation Framework Guide.

Framework Types

Type Idea Trade-off
Linear Recorded or straight-line scripts Quick to start, painful to maintain
Modular Application split into reusable modules/pages The foundation of POM
Data-driven Same script, many data sets (Excel, CSV, JSON, DB) Broad coverage without new code
Keyword-driven Actions defined as keywords in a table, mapped to code Non-coders can write tests; heavy to build and maintain
BDD Gherkin scenarios mapped to step definitions Business-readable; needs real collaboration
Hybrid A combination of the above The usual real-world choice

Data-driven vs hybrid

A data-driven framework separates test data from test logic, so one script runs against many rows. A hybrid framework combines several approaches — in most Selenium projects that means POM + data-driven, sometimes with BDD on top. Keyword-driven layers are rarer; describe a framework as including them only if it genuinely has a keyword table and interpreter.

Advertisement

Page Object Model

Each page (or reusable component, such as a header or date picker) is a class that holds:

  • Locators — private, so tests can't use them directly.
  • Actions — public methods named after what a user does: login(), searchFor(), addToCart().
  • State queries — methods that return information for tests to assert on: errorMessage(), isLoaded().

Tests create page objects and call their methods — they never touch locators or findElement().

Where do assertions go? Keep them in the tests. Page objects return values (getErrorMessage()); tests decide whether that value is right. Assertions buried in page objects hide what a test checks and make pages hard to reuse.

Benefits

  • A changed locator is fixed in one place, not in every test.
  • Tests read like user journeys.
  • Less duplication; new tests reuse existing page methods.
  • Page methods can return the next page object, so flows chain naturally.
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 type(By by, String text) {
        WebElement el = wait.until(ExpectedConditions.visibilityOfElementLocated(by));
        el.clear();
        el.sendKeys(text);
    }

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

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

public class LoginPage extends BasePage {
    private final By username = By.id("user-name");
    private final By password = By.id("password");
    private final By loginBtn = By.id("login-button");
    private final By error = By.cssSelector("[data-test='error']");

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

    public ProductsPage loginAs(String user, String pass) {
        type(username, user);
        type(password, pass);
        click(loginBtn);
        return new ProductsPage(driver);         // the next page in the flow
    }

    public String errorMessage() {
        return text(error);
    }
}

And the test:

@Test
public void lockedOutUserSeesError() {
    LoginPage login = new LoginPage(driver);
    login.loginAs("locked_out_user", "secret_sauce");
    Assert.assertTrue(login.errorMessage().contains("locked out"));
}

Every element is found fresh when it's used, with an explicit wait — so there's nothing to initialise and no stale references.

Building a Page Object — PageFactory and @FindBy

PageFactory is Selenium's built-in helper for declaring elements with annotations:

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 = "user-name")
    private WebElement usernameInput;

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

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

    public LoginPage(WebDriver driver) {          // the driver must be passed in
        this.driver = driver;
        PageFactory.initElements(driver, this);   // fills every @FindBy field
    }

    public void login(String user, String pass) {
        usernameInput.sendKeys(user);
        passwordInput.sendKeys(pass);
        loginButton.click();
    }
}
  • @FindBy declares how to find the element (id, name, css, xpath, className, linkText…).
  • PageFactory.initElements(driver, this) creates a proxy for each field. The real element is looked up each time you use it — lazily — not when the page object is created.
  • @FindBys chains locators (all must match, parent → child); @FindAll matches any of several locators. A List<WebElement> field collects multiple elements.
  • @CacheLookup caches the element after the first lookup — only safe for elements that never re-render.

What happens without initialisation?

@FindBy is only an annotation. Without initElements(), every field stays null, and the first action throws a NullPointerException — even though the locator is correct. It's the single most common POM bug. More causes and fixes: NullPointerException in Selenium.

PageFactory or By Locators?

  PageFactory (@FindBy) By locators
Setup Needs initElements() Nothing to initialise
Waits Proxies don't wait for visibility or clickability by themselves Easy to combine with explicit waits
Stale elements Possible with @CacheLookup or re-rendered lists Rare — elements are found fresh
Readability Compact annotations Explicit, slightly more code
Common in Many existing frameworks and tutorials Many newer frameworks

Both are valid. Know PageFactory for interviews and existing code; for new frameworks, many teams prefer By fields with wait helpers in a BasePage.

Common POM Mistakes

  • Forgetting initElements() — NullPointerException.
  • Assertions inside page objects — tests no longer show what they check.
  • "God" pages — one class for the whole app; split by page and component.
  • Locators leaking into tests — defeats the pattern.
  • Hard-coded waits in page methods — use explicit waits.
  • A static shared driver — breaks parallel runs; pass the driver in or use ThreadLocal.

See how locating and interacting with elements works under the hood in the Selenium WebDriver Visualizer. Build a complete framework step by step with the Framework From Scratch Roadmap.

From Real Projects

The framework I worked with was a hybrid framework in Java: Selenium WebDriver and TestNG, POM classes for each page, a business library for reusable flows, and Excel test data read with Apache POI. Part of my role was creating POM classes, adding elements when the application changed, extending the business library, and reviewing scripts assigned to me. That structure is what kept the suite maintainable as Canolog and Testsigma grew. One page class per screen, with no assertions inside page classes.

FAQs

What is the Page Object Model?

A design pattern where each page or component is a class holding its locators and user actions, so tests call page methods and locator changes happen in one place.

Is Page Object Model a framework?

No — it's a design pattern used inside a framework. The framework also includes the runner, driver management, data, reporting and CI.

What is PageFactory?

Selenium's helper that initialises @FindBy-annotated fields with lazy proxies when you call PageFactory.initElements(driver, this).

What happens if you don't call initElements()?

The @FindBy fields stay null and the first action throws a NullPointerException.

Should assertions be in page objects?

Generally no — page objects return values and tests assert on them, which keeps tests readable and pages reusable.

What is a hybrid framework?

A combination of approaches — usually Page Object Model with data-driven testing, sometimes with BDD.