Recently I have been researching better ways to write my Selenium tests as they have become rather hard to maintain. One pattern that I have found immensely useful is the Page Object Model (POM). In this blog post I will talk about what makes the POM a good pattern and show a simple framework I put together for getting up and running with the pattern.

The basic idea behind the pattern is to build classes that represent each page of an application. These classes generally have the following capabilities:

  1. One class per page, modal dialog, or logical section of the page
  2. Methods that represent actions that can be taken on a page. Some actions may transition to another page, others may just manipulate data on the current page.
  3. Accessor methods to query data or other content on a page

The essential benefits that you get from doing this are:

  • Cleaner test code — embedding selenium code in tests leads to cluttered, unreadable code
  • More readable code — with page objects, tests read like a path through the application
  • More maintainable test code — selenium locators are kept in the page objects where they can be easily found and modified if necessary

I do have to mention one important capability that this pattern does not provide. It does not provide a simple way to test for page layout. It is focused primarily on content. To test for layout, there are various tools that use snapshots to test expected page layout.

Some best practices I have found to be true:

  • Leave assertions out of the page objects. There are arguments to be made that a page object should check and make sure the appropriate actual page has been loaded or could do other types of test assertions. However this tends to make the page objects much more complicated than they need to be. Keep them simple. Provide accessors and use them to test page state. In addition, keeping assertions to only the test code makes it obvious what the test conditions are; embedding assertions in page objects, makes some test conditions implicit, which can be confusing. Also, Martin Fowler advises against them in most cases, so I do as I’m told 🙂
  • Use a factory to create the page objects. Ill provide a simple example of this but using a factory ensures, among other things, (1) that page objects are created uniformly, (2) that they all use the same WebDriver instance, and (3) initialization of the objects can be changed once and it is then applied to all page objects.
  • Use Selenium @FindBy annotations for locating static page elements. For those elements for which it is possible to define an unchanging locator (i.e., a static button that has a unique ID), this provides a simple way to define locators such that they aren’t embedded in code and are easy to find, if they need to change.

Now, here is the simple page object model framework that I have built.

pom-object-model

A few highlights to note:

  • The newPage() method in PageObjectFactory will create a new instance of any Page Object. PageObjects have publicly available constructors but this is the preferred mechanism for creating new instances.
  • PageObject is an abstract class that implements the init() method which is called in the newPage() method.
  • Initializing the PageObjectFactory once with a WebDriver and then using newPage() ensures that every page object instance has the proper WebDriver and has access to the factory to create other page objects.
  • Not pictured here is a delay implemented in the page creation process that allows time for the actual web page to load prior to returning the new PageObject instance
  • The example pages show both data accessor and action methods. Action methods that transition to another page return an instance of the new page.
    • Data accessor: getQuantityInCart()
    • Action: removeItemFromCart()

Here is a very simple page object representing the page showing product search results. Note that the getSelenium() method returns a helper library that I wrote to encapsulate some common selenium commands. It is not essential to the pattern.

public class SearchResultsPage extends PageObject {

  @FindBy(xpath = "//div[@id='slp-facet-wrap']/section/div/div/h1")
  private WebElement searchTitle;

  public ProductDetailsPage selectProduct(String productName) {
    getSelenium().scrollToClick(Locator.PARTIAL_LINK, productName);
    return factory.newPage(ProductDetailsPage.class);
  }
	
  public String getSearchTitle () {
    return searchTitle.getText();
  }
}

 

Here is some code that uses this mechanism in a test:

@Before
public final void setUp() throws Exception {
  this.driver = new ChromeDriver(DesiredCapabilities.chrome());
  driver.manage().deleteAllCookies();
  driver.manage().window().setSize(new Dimension(375, 1000));
		
  PageObjectFactory factory = PageObjectFactory.newInstance(driver, "http://foo.com");
  // Navigate to the default homepage
  this.homePage = factory.newPage(FooHomePage.class);
}

@Test
public void testTargetMobileThreeSpeakersNew() throws Exception {

  SearchResultsPage searchResultsPage = homePage.searchFor(productType);
  ProductDetailsPage productDetailsPage = searchResultsPage.selectProduct(productName);
  assertTrue(searchResultsPage.getPageTitle().startsWith(productName));
  ShoppingCartConfirmDialog cartConfirmDialog = productDetailsPage.addQuantityToCart(itemCount);
  ShoppingCartPage cartPage = cartConfirmDialog.clickViewCartAndCheckOut();

  int actualCountInCart = cartPage.getQuantityInCart(productName);
  String cartPageSummaryText = cartPage.getCartSummaryText();

  assertEquals(itemCount, actualCountInCart);
  assertNotNull(cartPageSummaryText);
  assertTrue(cartPageSummaryText.startsWith("cart total:"));

}

Leave a comment

Your email address will not be published. Required fields are marked *

X