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()vsdriver.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