Skills101

Software Testing & Automation · Lesson 7 of 11 · Updated ·

Page Object Model (POM) — Designing a Maintainable Selenium Framework

Imagine 200 tests that each contain driver.findElement(By.id("login-btn")). The developer renames the button's id — and 200 tests break. The Page Object Model puts each page's locators and actions in one class, so a change is fixed in one place.

LoginPageTest talks to LoginPage and DashboardPage, which talk to WebDriver; when the HTML changes only the page class needs fixing

1. A page class

// File: src/test/java/com/skills101/qa/pages/LoginPage.java
package com.skills101.qa.pages;

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

/** Page Object: everything a test may do on the login page — locators live here, not in tests. */
public class LoginPage {

    private final WebDriver driver;

    private final By email = By.id("email");
    private final By password = By.id("password");
    private final By loginButton = By.id("login-btn");
    private final By error = By.id("error");

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

    public LoginPage open(String baseUrl) {
        driver.get(baseUrl + "/login.html");
        return this;
    }

    public DashboardPage loginAs(String userEmail, String userPassword) {
        typeCredentials(userEmail, userPassword);
        return new DashboardPage(driver);
    }

    public LoginPage loginExpectingError(String userEmail, String userPassword) {
        typeCredentials(userEmail, userPassword);
        return this;
    }

    public String errorMessage() {
        return driver.findElement(error).getText();
    }

    private void typeCredentials(String userEmail, String userPassword) {
        driver.findElement(email).clear();
        driver.findElement(email).sendKeys(userEmail);
        driver.findElement(password).sendKeys(userPassword);
        driver.findElement(loginButton).click();
    }
}

Rules that make page objects good:

  • Locators are private By fields — tests never see them.
  • Methods are user actions (loginAs), not element pokes (clickButton3).
  • Methods return the next page: a successful login returns a DashboardPage; a failed one stays on LoginPage. Tests read like a story and can't call a method on the wrong page.
  • No assertions inside pages — pages do things and read values; tests decide what's correct.
  • Store By, not WebElement, so elements are found fresh (no stale elements).
// File: src/test/java/com/skills101/qa/pages/DashboardPage.java
package com.skills101.qa.pages;

import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

public class DashboardPage {

    private final WebDriver driver;

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

    public String welcomeText() {
        return driver.findElement(By.id("welcome")).getText();
    }

    public List<String> courseNames() {
        return driver.findElements(By.cssSelector("#my-courses tbody td:first-child"))
                .stream().map(WebElement::getText).toList();
    }

    public LoginPage logout() {
        driver.findElement(By.id("logout")).click();
        return new LoginPage(driver);
    }
}

2. Tests become short and readable

// File: src/test/java/com/skills101/qa/ui/LoginPageTest.java
package com.skills101.qa.ui;

import static org.testng.Assert.assertEquals;

import com.skills101.qa.base.BaseUiTest;
import com.skills101.qa.pages.DashboardPage;
import com.skills101.qa.pages.LoginPage;
import java.util.List;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

// Tests read like the user's story; if the page's HTML changes, only the page objects change
public class LoginPageTest extends BaseUiTest {

    @Test(groups = "smoke")
    public void validLoginShowsDashboard() {
        DashboardPage dashboard = new LoginPage(driver).open(baseUrl).loginAs("asha@skills101.in", "asha123");

        assertEquals(dashboard.welcomeText(), "Welcome, Asha!");
        assertEquals(dashboard.courseNames(), List.of("Java Full Stack", "DSA in Python"));
    }

    @DataProvider(name = "badLogins")
    public Object[][] badLogins() {
        return new Object[][] {
            {"asha@skills101.in", "wrong-password"},
            {"nobody@skills101.in", "asha123"},
            {"", ""},
        };
    }

    // runs once per row of the data provider
    @Test(dataProvider = "badLogins")
    public void invalidLoginShowsError(String email, String password) {
        LoginPage page = new LoginPage(driver).open(baseUrl).loginExpectingError(email, password);

        assertEquals(page.errorMessage(), "Wrong email or password");
    }
}

All four tests pass (one valid login plus three rows from the data provider). Compare with FirstSeleniumTest from lesson 2 — same behaviour, but no locators in the test.

(Selenium also has PageFactory with @FindBy fields; many teams now prefer plain By fields as shown, which are simpler and avoid stale elements.)

3. The whole framework

Framework layout: pom.xml, testng.xml, base classes, page objects, UI tests, API tests, a Postman collection and CI files

PartOur files
Driver managementDriverFactory (browser + headless from -D properties)
Test lifecycleBaseUiTest (open, screenshot on failure, quit)
Page objectsLoginPage, DashboardPage
Testsui/…, api/…
Configuration-Dbrowser, -Dheadless, -DapiUrl (no URLs hard-coded in tests)
ReportingTestLogger listener + Surefire reports
CIGitHub Actions workflow, Jenkinsfile (lesson 10)

When an interviewer says "explain your framework", describe these layers, why each exists, and one problem you solved (e.g. "flaky waits — replaced sleeps with explicit waits").

Common mistakes

  • Assertions inside page classes.
  • One giant page class for the whole site — make one per page or component (header, cart).
  • Tests that still use driver.findElement directly next to page objects.
  • Hard-coded URLs, users and passwords inside tests.

Quick recap

  • POM: one class per page holds its locators and actions.
  • Methods = user actions, returning the next page; no assertions in pages.
  • Framework layers: driver factory, base test, pages, tests, config, reports, CI.

Practice what you learned

📝 Quiz: Page Object Model5 questions · instant answers & explanations · +10 XP · counts towards your free certificateTake the quiz →