Page Object Model

Updated: 06 February 2023

Taken from https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/

Page Object is a Design Pattern that has become popular in test automation. A page object is an object-oriented class that serves as an interface to a page of the AUT. The tests then use the methods of this page object class whenever they need to interact with the UI of that page. The benefit is that if the UI changes for the page, the tests themselves don’t need to change, only the code within the page object needs to change.

There is a single repository for the services or operations the page offers rather than having these services scattered throughout the tests.

Methods offering the services of a page might return more Page Objects e.g. a click on a Compose mail button could return a ComposeMail class object.

A login test (java), without Gherkin, might look like:

public class TestLogin {

  @Test
  public void testLogin() {
    SignInPage signInPage = new SignInPage(driver);
    HomePage homePage = signInPage.loginValidUser("userName", "password");
    assertThat(homePage.getMessageText(), is("Hello userName"));
  }
}

Assertions in Page Objects

The page object will contain the representation of the page, and the services the page provides via methods but it should not contain assertions.

There is one, single, verification which can, and should, be within the page object and that is to verify that the page, and possibly critical elements on the page, were loaded correctly. This verification should be done while instantiating the page object

public class SignInPage {

  // ...

  public SignInPage(WebDriver driver){
    this.driver = driver;
     if (!driver.getTitle().equals("Sign In Page")) {
      throw new IllegalStateException("This is not Sign In Page," +
            " current page is: " + driver.getCurrentUrl());
    }
  }

  public HomePage login(String username, String password) {
    // ... 
  }
    
  public LoginPage loginExpectingError(String username, String password) {
    //  ...
  }
    
  public String getErrorMessage() {
    // verify that the correct error is shown
  }
}

Page Component Objects

A page object does not necessarily need to represent all the parts of a page itself. The same principles used for page objects can be used to create “Page Component Objects” that represent discrete chunks of the page and can be included in page objects. These component objects can provide references to the elements inside those discrete chunks, and methods to leverage the functionality provided by them.

You can even nest component objects inside other component objects for more complex pages. If a page in the AUT has multiple components, or common components used throughout the site (e.g. a navigation bar), then it may improve maintainability and reduce code duplication.

Implementation Notes

PageObjects can be thought of as facing in two directions simultaneously. Facing toward the developer of a test, they represent the services offered by a particular page. Facing away from the developer, they should be the only thing that has a deep knowledge of the structure of the HTML of a page (or part of a page).

Methods on the PageObject should return other PageObjects. This means we can effectively model the user journey through our application. It also means that should the way that pages relate to one another change (like when the login page asks the user to change their password the first time they log into a service when it previously didn't do that), simply changing the appropriate method’s signature will cause the tests to fail to compile. Put another way; we can tell which tests would fail without needing to run them when we change the relationship between pages and reflect this in the PageObjects.

Summary

  • The public methods represent the services that the page offers
  • Try not to expose the internals of the page
  • Generally don’t make assertions
  • Methods return other PageObjects
  • Need not represent an entire page
  • Different results for the same action are modelled as different methods

Complete login page example

public class LoginPage {
  private final WebDriver driver;

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

    // Check that we're on the right page.
    if (!"Login".equals(driver.getTitle())) {
      // Alternatively, we could navigate to the login page, perhaps
      // logging out first
      throw new IllegalStateException("This is not the login page");
    }
  }

  // The page contains several HTML elements that will be represented as
  // WebElements. The locators for these should only be defined once.
  By usernameLocator = By.id("username");
  By passwordLocator = By.id("passwd");
  By loginButtonLocator = By.id("login");

  // The login page allows the user to type their username into
  // the username field.
  public LoginPage typeUsername(String username) {
    // This is the only place that "knows" how to enter a username
    driver.findElement(usernameLocator).sendKeys(username);

    // Return the current page object as this action doesn't navigate
    // to a page represented by another PageObject
    return this;	
  }

  // The login page allows the user to type their password into
  // the password field.
  public LoginPage typePassword(String password) {
    // This is the only place that "knows" how to enter a password
    driver.findElement(passwordLocator).sendKeys(password);

    // Return the current page object as this action doesn't navigate
    // to a page represented by another PageObject
    return this;	
  }

  // The login page allows the user to submit the login form
  public HomePage submitLogin() {
    // This is the only place that submits the login form and expects
    // the destination to be the home page. A seperate method should be
    // created for the instance of clicking login whilst expecting
    // a login failure. 
    driver.findElement(loginButtonLocator).submit();

    // Return a new page object representing the destination. Should the
    // login page ever go somewhere else (for example, a legal disclaimer)
    // then changing the method signature for this method will mean that
    // all tests that rely on this behaviour won't compile.
    return new HomePage(driver);	
  }

  // The login page allows the user to submit the login form knowing that
  // an invalid username and / or password were entered.
  public LoginPage submitLoginExpectingFailure() {
    // This is the only place that submits the login form and expects the
    // destination to be the login page due to login failure.
    driver.findElement(loginButtonLocator).submit();

    // Return a new page object representing the destination. Should the
    // user ever be navigated to the home page after submiting a login
    // with credentials expected to fail login, the script will fail when
    // it attempts to instantiate the LoginPage PageObject.
    return new LoginPage(driver);	
  }

  // Conceptually, the login page offers the user the service of being
  // able to "log into" the application using a user name and password. 
  public HomePage loginAs(String username, String password) {
    // The PageObject methods that enter username, password & submit
    // login have already defined and should not be repeated here.
    typeUsername(username);
    typePassword(password);
    return submitLogin();
  }
}