Page Object Model (POM)
Look back at the last two lessons: By.id("email"), By.id("password"), and By.id("loginBtn") appear, hardcoded, inside every single test method that touches the login page. Now imagine RecipeBox's frontend team renames id="loginBtn" to id="submitLoginBtn". Every test class with that locator baked in breaks, and you have to hunt down and fix each occurrence by hand. That's the maintenance problem the Page Object Model (POM) exists to solve.
The Problem: Locators Scattered Everywhere
| Without POM | With POM |
|---|---|
| Every test class calls driver.findElement(By.id("loginBtn")) directly | Only LoginPage knows that locator exists |
| A UI change means editing N test files | A UI change means editing 1 page class |
| Tests read like raw browser commands | Tests read like business actions: loginPage.login(email, pass) |
| No reuse — copy-pasted findElement calls everywhere | Page methods are reused across every test that needs that page |
The Pattern: One Class Per Page
Each meaningful screen in the application gets its own Java class. That class holds:
- The locators for that page's elements (usually as private fields)
- Action methods that perform a meaningful operation on that page (e.g. login(...), addRecipe(...), search(...))
- Optionally, methods that return information from the page (e.g. getErrorMessage())
Test classes then talk to the page through these methods only — they never call driver.findElement() directly for that page's elements again.
Worked Example: LoginPage
public class LoginPage {
private final WebDriver driver;
private final WebDriverWait wait;
// Locators — the ONLY place these ids/classes should appear in the whole framework
private final By emailField = By.id("email");
private final By passwordField = By.id("password");
private final By loginButton = By.id("loginBtn");
private final By errorBanner = By.className("login-error");
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
public void open() {
driver.get("https://recipebox.example.com/login");
}
// The action method every test calls instead of touching findElement directly
public void login(String email, String password) {
driver.findElement(emailField).clear();
driver.findElement(emailField).sendKeys(email);
driver.findElement(passwordField).clear();
driver.findElement(passwordField).sendKeys(password);
driver.findElement(loginButton).click();
}
public String getErrorMessage() {
WebElement error = wait.until(ExpectedConditions.visibilityOfElementLocated(errorBanner));
return error.getText();
}
public boolean isLoginSuccessful() {
wait.until(ExpectedConditions.urlContains("/recipes"));
return driver.getCurrentUrl().contains("/recipes");
}
}
Using LoginPage From a Test Class
public class RecipeBoxLoginPomTest {
private WebDriver driver;
private LoginPage loginPage;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
loginPage = new LoginPage(driver);
loginPage.open();
}
@Test
public void validLoginRedirectsToRecipeList() {
loginPage.login("tester@example.com", "Test@1234");
Assert.assertTrue(loginPage.isLoginSuccessful(),
"Valid credentials should redirect to the recipe list");
}
@Test
public void wrongPasswordShowsError() {
loginPage.login("tester@example.com", "WrongPassword");
Assert.assertEquals(loginPage.getErrorMessage(), "Invalid email or password.");
}
@AfterMethod
public void tearDown() {
if (driver != null) driver.quit();
}
}
Notice what disappeared: no By.id(...), no raw driver.findElement(...) anywhere in the test class. If RecipeBox renames loginBtn to submitLoginBtn tomorrow, you change one line inside LoginPage and every test that uses it keeps working.
Scaling the Pattern: AddRecipePage
The same shape applies to every other screen — here's the skeleton for RecipeBox's "Add Recipe" page:
public class AddRecipePage {
private final WebDriver driver;
private final By titleField = By.id("recipeTitle");
private final By categoryDropdown = By.id("categoryDropdown");
private final By saveButton = By.id("saveRecipeBtn");
public AddRecipePage(WebDriver driver) {
this.driver = driver;
}
public void addRecipe(String title, String category) {
driver.findElement(titleField).sendKeys(title);
driver.findElement(categoryDropdown).sendKeys(category);
driver.findElement(saveButton).click();
}
}
- A page object should never contain assertions — that stays in the test class. The page class exposes state (getErrorMessage()), the test decides if that state is correct.
- Action methods should represent a real user action (login, addRecipe, searchFor) — not a 1:1 wrapper around every single findElement call.
- Locators are private. Nothing outside the page class should know or care what a locator actually is.