Jobs Platform
Selenium with Java and TestNG: Level 1

Lesson 8 of 8 · 8 min

Page Object Model: keeping tests clean

When you write your first tests, locators and steps sit right inside the test methods. This works for five tests. At fifty tests, one change to the login button means editing fifty places. The Page Object Model (POM) solves this. It is the most common design pattern in Selenium projects, and interviewers ask about it often.

The idea

Create one Java class for each page, or part of a page. The class keeps the locators for that page and offers methods for what a user can do there. The test calls those methods and knows nothing about ids or CSS. The Selenium documentation describes the same principle: only the page object should know the HTML structure of the page, and changes to the page are fixed in one place.

A page object

package demo.pages;

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

public class LoginPage {
    private final WebDriver driver;
    private final By username = By.id("username");
    private final By password = By.name("pass");
    private final By loginButton = By.cssSelector("button#login");
    private final By message = By.id("msg");

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

    public LoginPage login(String user, String pass) {
        driver.findElement(username).sendKeys(user);
        driver.findElement(password).sendKeys(pass);
        driver.findElement(loginButton).click();
        return this;
    }

    public String messageText() {
        return driver.findElement(message).getText();
    }
}

Notice three things. The locators are private By fields, so the test cannot touch them. The elements are found only when a method runs, which avoids stale elements. And login() returns a page object, which allows chaining.

The test using it

The BaseTest from the last lesson opens example.com, which has no login form. So the test first opens a small practice page, saved as src/test/resources/login.html:

<!DOCTYPE html>
<html><head><title>Login</title></head>
<body>
<input id="username" type="text">
<input name="pass" type="password">
<button id="login" type="button"
  onclick="document.getElementById('msg').innerText = 'Welcome ' + document.getElementById('username').value">Login</button>
<p id="msg"></p>
</body></html>
package demo;

import demo.pages.LoginPage;
import java.nio.file.Path;
import org.testng.Assert;
import org.testng.annotations.Test;

public class LoginTest extends BaseTest {
    @Test
    public void validLogin() {
        driver.get(Path.of("src/test/resources/login.html").toUri().toString());
        LoginPage page = new LoginPage(driver);
        page.login("Asha", "secret");
        Assert.assertEquals(page.messageText(), "Welcome Asha");
    }
}

Because the testng.xml from the last lesson runs only the smoke group, run this one class with mvn test -Dtest=LoginTest. The output ends with:

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

The test reads like a short story: log in, then check the message. If the login button gets a new id tomorrow, you change one line in LoginPage and all tests keep working.

Rules that keep a framework healthy

  • One class per page or component. Put locators and user actions in it.
  • Do not put assertions in page objects. The Selenium documentation recommends that assertions stay in the tests, so the page object only reports what it sees.
  • Return another page object when an action moves to a new page, for example login() returning a HomePage.
  • Keep browser setup and quit in a base test class, as in the previous lesson.
  • Make tests independent. Each test starts a fresh browser and does not rely on another test having run.

A typical Maven layout puts page classes in src/test/java/pages, tests in src/test/java/tests, and the base class and utilities beside them. You may also meet PageFactory and the @FindBy annotation, an older helper for the same pattern. Plain By fields, as above, are simple and clear for a fresher.

Try it yourself

  • Take any test you wrote earlier and move its locators and steps into a page class.
  • Add a method to LoginPage that returns true if an error message is displayed.
  • Create a second page class, HomePage, and make a successful login() return it.
  • Change a locator in your page class and confirm that no test code needed editing.

Quick check

Three quick questions on this lesson. Your answers are not saved.

  1. Question 1 of 3

    A site redesign changes the id of the search box used in 30 tests. With Page Object Model, how many files need a locator change?

  2. Question 2 of 3

    Where should assertions normally be written in a Page Object Model project?

  3. Question 3 of 3

    Why store locators as By fields instead of storing found WebElement objects?