Page Object Model

Design patterns for test automation

Introduction to Page Object Model

Page Object Model (POM) is a design pattern that creates an object repository for web UI elements. It separates test logic from page-specific code, making tests more readable, maintainable, and reusable.

Why Use Page Object Model?

Without POM (Bad Practice)

// Test 1: Login Test
@Test
public void testLogin() {
    driver.findElement(By.id("username")).sendKeys("user1");
    driver.findElement(By.id("password")).sendKeys("pass123");
    driver.findElement(By.id("loginBtn")).click();
}

// Test 2: Another test using same elements
@Test
public void testInvalidLogin() {
    driver.findElement(By.id("username")).sendKeys("invalid");
    driver.findElement(By.id("password")).sendKeys("wrong");
    driver.findElement(By.id("loginBtn")).click();
}

// Problem: If "loginBtn" ID changes to "submitBtn",
// you must update EVERY test file!

With POM (Best Practice)

// LoginPage.java - Single source of truth
public class LoginPage {
    private By username = By.id("username");
    private By password = By.id("password");
    private By loginBtn = By.id("loginBtn");

    public void login(String user, String pass) {
        driver.findElement(username).sendKeys(user);
        driver.findElement(password).sendKeys(pass);
        driver.findElement(loginBtn).click();
    }
}

// Tests - Clean and readable
@Test
public void testLogin() {
    loginPage.login("user1", "pass123");
}

@Test
public void testInvalidLogin() {
    loginPage.login("invalid", "wrong");
}

// If locator changes, update only LoginPage.java!

POM Benefits Summary

  • Single Point of Maintenance: Locators defined once, used everywhere
  • Code Reusability: Page methods reused across test suites
  • Better Readability: loginPage.login() vs driver.findElement().click()
  • Easier Debugging: Issues isolated to specific page classes
  • Team Collaboration: Clear structure for multiple developers

Project Structure

A well-organized POM project follows a clear folder structure separating pages, tests, utilities, and configurations.

Recommended Project Structure

selenium-pom-framework/
├── src/
│   ├── main/
│   │   └── java/
│   │       ├── pages/
│   │       │   ├── BasePage.java
│   │       │   ├── LoginPage.java
│   │       │   ├── HomePage.java
│   │       │   ├── ProductPage.java
│   │       │   └── CheckoutPage.java
│   │       ├── utils/
│   │       │   ├── DriverFactory.java
│   │       │   ├── ConfigReader.java
│   │       │   └── WaitHelper.java
│   │       └── constants/
│   │           └── Constants.java
│   │
│   └── test/
│       ├── java/
│       │   ├── base/
│       │   │   └── BaseTest.java
│       │   └── tests/
│       │       ├── LoginTest.java
│       │       ├── HomePageTest.java
│       │       └── CheckoutTest.java
│       └── resources/
│           ├── testng.xml
│           └── config.properties
│
├── pom.xml
└── README.md

Maven Dependencies (pom.xml)

<dependencies>
    <!-- Selenium WebDriver -->
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.27.0</version>
    </dependency>

    <!-- TestNG -->
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.10.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Creating Base Page Class

The BasePage class contains common methods and the WebDriver instance that all page classes inherit.

BasePage.java

package pages;

import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.*;
import java.time.Duration;

public class BasePage {
    protected WebDriver driver;
    protected WebDriverWait wait;
    private static final int TIMEOUT = 10;

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

    // Wait for element to be visible
    protected WebElement waitForVisibility(By locator) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    // Wait for element to be clickable
    protected WebElement waitForClickable(By locator) {
        return wait.until(ExpectedConditions.elementToBeClickable(locator));
    }

    // Click element with wait
    protected void click(By locator) {
        waitForClickable(locator).click();
    }

    // Type text with clear
    protected void type(By locator, String text) {
        WebElement element = waitForVisibility(locator);
        element.clear();
        element.sendKeys(text);
    }

    // Get text from element
    protected String getText(By locator) {
        return waitForVisibility(locator).getText();
    }

    // Check if element is displayed
    protected boolean isDisplayed(By locator) {
        try {
            return waitForVisibility(locator).isDisplayed();
        } catch (TimeoutException e) {
            return false;
        }
    }

    // Select dropdown by visible text
    protected void selectByText(By locator, String text) {
        Select dropdown = new Select(waitForVisibility(locator));
        dropdown.selectByVisibleText(text);
    }

    // Get current page title
    public String getPageTitle() {
        return driver.getTitle();
    }

    // Get current URL
    public String getCurrentUrl() {
        return driver.getCurrentUrl();
    }

    // Navigate to URL
    protected void navigateTo(String url) {
        driver.get(url);
    }
}

BasePage Benefits

  • Centralizes WebDriver instance management
  • Provides reusable methods for common actions
  • Built-in waits prevent flaky tests
  • Reduces code duplication across page classes
  • Easy to add new common functionality

Creating Page Classes

Each page class represents a single page in your application and contains locators and methods specific to that page.

LoginPage.java

package pages;

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

public class LoginPage extends BasePage {

    // Locators
    private By usernameField = By.id("username");
    private By passwordField = By.id("password");
    private By loginButton = By.id("loginBtn");
    private By errorMessage = By.className("error-message");
    private By forgotPasswordLink = By.linkText("Forgot Password?");
    private By rememberMeCheckbox = By.id("rememberMe");

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

    // Page Actions
    public void enterUsername(String username) {
        type(usernameField, username);
    }

    public void enterPassword(String password) {
        type(passwordField, password);
    }

    public void clickLogin() {
        click(loginButton);
    }

    // Combined action - returns next page
    public HomePage login(String username, String password) {
        enterUsername(username);
        enterPassword(password);
        clickLogin();
        return new HomePage(driver);
    }

    // For invalid login - stays on same page
    public LoginPage loginExpectingFailure(String username, String password) {
        enterUsername(username);
        enterPassword(password);
        clickLogin();
        return this;
    }

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

    public boolean isErrorDisplayed() {
        return isDisplayed(errorMessage);
    }

    public void clickForgotPassword() {
        click(forgotPasswordLink);
    }

    public void checkRememberMe() {
        click(rememberMeCheckbox);
    }

    public boolean isLoginPageDisplayed() {
        return isDisplayed(loginButton);
    }
}

HomePage.java

package pages;

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

public class HomePage extends BasePage {

    // Locators
    private By welcomeMessage = By.id("welcomeMsg");
    private By userProfileIcon = By.id("userProfile");
    private By logoutButton = By.id("logoutBtn");
    private By searchBox = By.id("searchInput");
    private By searchButton = By.id("searchBtn");
    private By cartIcon = By.id("cartIcon");
    private By cartCount = By.className("cart-count");

    // Constructor
    public HomePage(WebDriver driver) {
        super(driver);
    }

    // Page Actions
    public String getWelcomeMessage() {
        return getText(welcomeMessage);
    }

    public boolean isUserLoggedIn() {
        return isDisplayed(welcomeMessage);
    }

    public void clickUserProfile() {
        click(userProfileIcon);
    }

    public LoginPage logout() {
        click(userProfileIcon);
        click(logoutButton);
        return new LoginPage(driver);
    }

    public SearchResultsPage search(String keyword) {
        type(searchBox, keyword);
        click(searchButton);
        return new SearchResultsPage(driver);
    }

    public CartPage goToCart() {
        click(cartIcon);
        return new CartPage(driver);
    }

    public int getCartItemCount() {
        String count = getText(cartCount);
        return Integer.parseInt(count);
    }
}

ProductPage.java

package pages;

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

public class ProductPage extends BasePage {

    // Locators
    private By productTitle = By.id("productName");
    private By productPrice = By.id("productPrice");
    private By quantityInput = By.id("quantity");
    private By addToCartBtn = By.id("addToCart");
    private By sizeDropdown = By.id("sizeSelect");
    private By successMessage = By.className("success-msg");

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

    public String getProductTitle() {
        return getText(productTitle);
    }

    public String getProductPrice() {
        return getText(productPrice);
    }

    public ProductPage setQuantity(int qty) {
        type(quantityInput, String.valueOf(qty));
        return this;
    }

    public ProductPage selectSize(String size) {
        selectByText(sizeDropdown, size);
        return this;
    }

    public ProductPage addToCart() {
        click(addToCartBtn);
        return this;
    }

    public boolean isSuccessMessageDisplayed() {
        return isDisplayed(successMessage);
    }

    // Method chaining example
    public ProductPage addProductToCart(String size, int quantity) {
        return selectSize(size)
                .setQuantity(quantity)
                .addToCart();
    }
}

PageFactory & @FindBy

PageFactory is a Selenium support class that provides an elegant way to initialize page elements using annotations.

LoginPage with PageFactory

package pages;

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

public class LoginPage {
    private WebDriver driver;

    // Using @FindBy annotation
    @FindBy(id = "username")
    private WebElement usernameField;

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

    @FindBy(id = "loginBtn")
    private WebElement loginButton;

    // Alternative syntax with How enum
    @FindBy(how = How.CLASS_NAME, using = "error-message")
    private WebElement errorMessage;

    @FindBy(css = "a[href='#forgot']")
    private WebElement forgotPasswordLink;

    @FindBy(xpath = "//input[@type='checkbox']")
    private WebElement rememberMeCheckbox;

    // Constructor - Initialize elements with PageFactory
    public LoginPage(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    // Page methods using WebElement directly
    public void enterUsername(String username) {
        usernameField.clear();
        usernameField.sendKeys(username);
    }

    public void enterPassword(String password) {
        passwordField.clear();
        passwordField.sendKeys(password);
    }

    public HomePage clickLogin() {
        loginButton.click();
        return new HomePage(driver);
    }

    public HomePage login(String username, String password) {
        enterUsername(username);
        enterPassword(password);
        return clickLogin();
    }

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

@FindBy Locator Options

Annotation                           | Equivalent
-------------------------------------+---------------------------
@FindBy(id = "elementId")            | By.id("elementId")
@FindBy(name = "elementName")        | By.name("elementName")
@FindBy(className = "class")         | By.className("class")
@FindBy(css = "div.class")           | By.cssSelector("div.class")
@FindBy(xpath = "//div[@id='x']")    | By.xpath("//div[@id='x']")
@FindBy(linkText = "Click Here")     | By.linkText("Click Here")
@FindBy(partialLinkText = "Click")   | By.partialLinkText("Click")
@FindBy(tagName = "input")           | By.tagName("input")

@FindAll and @FindBys

import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBys;
import java.util.List;

public class SearchResultsPage {

    // @FindAll - OR condition (match ANY locator)
    @FindAll({
        @FindBy(className = "product-card"),
        @FindBy(className = "product-item")
    })
    private List<WebElement> productItems;

    // @FindBys - AND condition (match ALL locators in chain)
    @FindBys({
        @FindBy(id = "results"),
        @FindBy(className = "product-title")
    })
    private List<WebElement> productTitles;

    public int getProductCount() {
        return productItems.size();
    }

    public List<String> getProductTitles() {
        return productTitles.stream()
                .map(WebElement::getText)
                .collect(Collectors.toList());
    }
}

AjaxElementLocatorFactory (With Waits)

import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;

public class LoginPage {

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        // Wait up to 10 seconds for elements
        PageFactory.initElements(
            new AjaxElementLocatorFactory(driver, 10),
            this
        );
    }
}

Creating Base Test Class

The BaseTest class handles WebDriver setup and teardown, providing a clean foundation for all test classes.

BaseTest.java

package base;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.*;
import pages.LoginPage;
import java.time.Duration;

public class BaseTest {
    protected WebDriver driver;
    protected LoginPage loginPage;

    protected static final String BASE_URL = "https://example.com";

    @Parameters({"browser"})
    @BeforeMethod
    public void setUp(@Optional("chrome") String browser) {
        driver = createDriver(browser);
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        driver.get(BASE_URL);

        // Initialize first page
        loginPage = new LoginPage(driver);
    }

    private WebDriver createDriver(String browser) {
        switch (browser.toLowerCase()) {
            case "firefox":
                return new FirefoxDriver();
            case "chrome":
            default:
                ChromeOptions options = new ChromeOptions();
                options.addArguments("--start-maximized");
                return new ChromeDriver(options);
        }
    }

    @AfterMethod
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

Thread-Safe BaseTest (Parallel Execution)

package base;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.*;

public class BaseTest {
    // ThreadLocal for parallel execution
    protected static ThreadLocal<WebDriver> driverThread = new ThreadLocal<>();

    protected static final String BASE_URL = "https://example.com";

    public static WebDriver getDriver() {
        return driverThread.get();
    }

    @BeforeMethod
    public void setUp() {
        WebDriver driver = new ChromeDriver();
        driverThread.set(driver);
        getDriver().manage().window().maximize();
        getDriver().get(BASE_URL);
    }

    @AfterMethod
    public void tearDown() {
        if (getDriver() != null) {
            getDriver().quit();
            driverThread.remove();
        }
    }
}

Writing Test Classes

Test classes extend BaseTest and use page objects to perform actions and assertions.

LoginTest.java

package tests;

import base.BaseTest;
import org.testng.Assert;
import org.testng.annotations.Test;
import pages.HomePage;

public class LoginTest extends BaseTest {

    @Test(description = "Verify successful login with valid credentials")
    public void testValidLogin() {
        // Use page object to perform login
        HomePage homePage = loginPage.login("testuser", "password123");

        // Verify login success
        Assert.assertTrue(homePage.isUserLoggedIn(),
            "User should be logged in");
        Assert.assertTrue(homePage.getWelcomeMessage().contains("Welcome"),
            "Welcome message should be displayed");
    }

    @Test(description = "Verify error message for invalid credentials")
    public void testInvalidLogin() {
        // Attempt login with invalid credentials
        loginPage.loginExpectingFailure("wronguser", "wrongpass");

        // Verify error is displayed
        Assert.assertTrue(loginPage.isErrorDisplayed(),
            "Error message should be displayed");
        Assert.assertEquals(loginPage.getErrorMessage(),
            "Invalid username or password");
    }

    @Test(description = "Verify login page is displayed")
    public void testLoginPageDisplayed() {
        Assert.assertTrue(loginPage.isLoginPageDisplayed(),
            "Login page should be displayed");
        Assert.assertEquals(loginPage.getPageTitle(), "Login - MyApp");
    }
}

HomePageTest.java

package tests;

import base.BaseTest;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import pages.HomePage;
import pages.SearchResultsPage;

public class HomePageTest extends BaseTest {
    private HomePage homePage;

    @BeforeMethod
    public void loginFirst() {
        // Login before each test
        homePage = loginPage.login("testuser", "password123");
    }

    @Test(description = "Verify search functionality")
    public void testSearch() {
        SearchResultsPage results = homePage.search("laptop");

        Assert.assertTrue(results.getProductCount() > 0,
            "Search should return results");
    }

    @Test(description = "Verify cart is initially empty")
    public void testEmptyCart() {
        Assert.assertEquals(homePage.getCartItemCount(), 0,
            "Cart should be empty initially");
    }

    @Test(description = "Verify logout functionality")
    public void testLogout() {
        loginPage = homePage.logout();

        Assert.assertTrue(loginPage.isLoginPageDisplayed(),
            "User should be redirected to login page");
    }
}

E2E Test: Complete Purchase Flow

package tests;

import base.BaseTest;
import org.testng.Assert;
import org.testng.annotations.Test;
import pages.*;

public class PurchaseFlowTest extends BaseTest {

    @Test(description = "Complete end-to-end purchase flow")
    public void testCompletePurchase() {
        // Step 1: Login
        HomePage homePage = loginPage.login("testuser", "password123");
        Assert.assertTrue(homePage.isUserLoggedIn());

        // Step 2: Search for product
        SearchResultsPage searchResults = homePage.search("iPhone");
        Assert.assertTrue(searchResults.getProductCount() > 0);

        // Step 3: Select first product
        ProductPage productPage = searchResults.selectFirstProduct();
        Assert.assertFalse(productPage.getProductTitle().isEmpty());

        // Step 4: Add to cart
        productPage.addProductToCart("128GB", 1);
        Assert.assertTrue(productPage.isSuccessMessageDisplayed());

        // Step 5: Go to cart and checkout
        CartPage cartPage = productPage.goToCart();
        Assert.assertEquals(cartPage.getItemCount(), 1);

        // Step 6: Complete checkout
        CheckoutPage checkoutPage = cartPage.proceedToCheckout();
        checkoutPage.fillShippingDetails("John Doe", "123 Main St");

        OrderConfirmationPage confirmationPage = checkoutPage.placeOrder();
        Assert.assertTrue(confirmationPage.isOrderConfirmed());
        Assert.assertFalse(confirmationPage.getOrderNumber().isEmpty());
    }
}

Notice the Clean Test Code

  • No direct WebDriver calls in tests
  • No locators visible in test methods
  • Tests read like user stories
  • Page transitions return new page objects
  • Easy to understand test flow

Fluent Interface & Method Chaining

Method chaining creates a fluent API that makes test code more readable and expressive.

Fluent Page Object

public class RegistrationPage extends BasePage {
    private By firstName = By.id("firstName");
    private By lastName = By.id("lastName");
    private By email = By.id("email");
    private By password = By.id("password");
    private By confirmPassword = By.id("confirmPassword");
    private By termsCheckbox = By.id("terms");
    private By submitButton = By.id("register");

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

    // Return 'this' for method chaining
    public RegistrationPage enterFirstName(String name) {
        type(firstName, name);
        return this;
    }

    public RegistrationPage enterLastName(String name) {
        type(lastName, name);
        return this;
    }

    public RegistrationPage enterEmail(String emailAddr) {
        type(email, emailAddr);
        return this;
    }

    public RegistrationPage enterPassword(String pass) {
        type(password, pass);
        return this;
    }

    public RegistrationPage confirmPassword(String pass) {
        type(confirmPassword, pass);
        return this;
    }

    public RegistrationPage acceptTerms() {
        click(termsCheckbox);
        return this;
    }

    public HomePage submit() {
        click(submitButton);
        return new HomePage(driver);
    }
}

Using Fluent API in Tests

@Test
public void testUserRegistration() {
    RegistrationPage registrationPage = new RegistrationPage(driver);

    // Fluent method chaining
    HomePage homePage = registrationPage
        .enterFirstName("John")
        .enterLastName("Doe")
        .enterEmail("john.doe@example.com")
        .enterPassword("SecurePass123")
        .confirmPassword("SecurePass123")
        .acceptTerms()
        .submit();

    Assert.assertTrue(homePage.isUserLoggedIn());
}

Best Practices

Good Practices Example

// Good: Descriptive method names
loginPage.login(username, password);
homePage.searchForProduct("laptop");
cartPage.removeItem(0);

// Good: Return appropriate page objects
HomePage homePage = loginPage.login(user, pass);  // Returns next page
LoginPage samePage = loginPage.loginExpectingFailure(user, pass);  // Stays on same page

// Good: Verification methods in page class
public boolean isLoginSuccessful() {
    return isDisplayed(welcomeMessage);
}

// Bad: Assertion in page class
public void verifyLogin() {
    Assert.assertTrue(isDisplayed(welcomeMessage));  // Don't do this!
}

Key Takeaways

  • Separation of Concerns: Page classes handle UI, Test classes handle assertions
  • Single Responsibility: Each page class represents one page only
  • DRY Principle: Locators defined once, used everywhere
  • Encapsulation: Hide implementation details, expose behaviors
  • Maintainability: UI changes require updates in one place

Complete Framework Example

testng.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="POM Test Suite" parallel="tests" thread-count="2">

    <test name="Chrome Tests">
        <parameter name="browser" value="chrome"/>
        <classes>
            <class name="tests.LoginTest"/>
            <class name="tests.HomePageTest"/>
            <class name="tests.PurchaseFlowTest"/>
        </classes>
    </test>

    <test name="Firefox Tests">
        <parameter name="browser" value="firefox"/>
        <classes>
            <class name="tests.LoginTest"/>
        </classes>
    </test>

</suite>

config.properties

# Application Settings
base.url=https://example.com
default.browser=chrome

# Timeouts
implicit.wait=10
explicit.wait=15
page.load.timeout=30

# Test Data
valid.username=testuser
valid.password=password123

ConfigReader.java

package utils;

import java.io.*;
import java.util.Properties;

public class ConfigReader {
    private static Properties properties;

    static {
        try {
            FileInputStream file = new FileInputStream("src/test/resources/config.properties");
            properties = new Properties();
            properties.load(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String get(String key) {
        return properties.getProperty(key);
    }

    public static String getBaseUrl() {
        return get("base.url");
    }

    public static int getImplicitWait() {
        return Integer.parseInt(get("implicit.wait"));
    }
}

Next Steps

Continue Your Learning Journey

  • Data-Driven Testing - Parameterize tests with Excel, CSV or database
  • Selenium Grid - Parallel execution across multiple browsers and machines
  • Extent Reports - Generate rich HTML reports with screenshots
  • CI/CD Integration - Automate test execution with Jenkins or GitHub Actions
  • BDD with Cucumber - Combine POM with Behavior-Driven Development
  • Docker & Selenium - Containerized test execution