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.
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
Byfields — 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 onLoginPage. 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, notWebElement, 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
| Part | Our files |
|---|---|
| Driver management | DriverFactory (browser + headless from -D properties) |
| Test lifecycle | BaseUiTest (open, screenshot on failure, quit) |
| Page objects | LoginPage, DashboardPage |
| Tests | ui/…, api/… |
| Configuration | -Dbrowser, -Dheadless, -DapiUrl (no URLs hard-coded in tests) |
| Reporting | TestLogger listener + Surefire reports |
| CI | GitHub 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.findElementdirectly 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.