Select Page
Selenium Testing

The Complete Guide to Page Object Model in Selenium

In this blog article, we will learn what ‘Page Object Model’ is and how to use it effectively.

The Complete Guide to Page Object Model in Selenium
Listen to this blog

Page Object Model is a design pattern for automation testing. It is easy to define what the Page Object Model pattern is. However, many automation testers use the Page Object model without knowing its value. In this blog article, we will learn what ‘Page Object Model’ is and how to use it effectively.

Let’s say your team is writing automation test scripts without Page Object Pattern. In that case, there are high chances of developing duplicate test steps. In the modern era, manual testing on multiple browsers and devices is a nightmare for a team. So your team needs to create automation scripts quickly and add the new features’ test scripts in the automated regression suite.

Without the Page Object Model, your team would need to have a lot of discussions and reviewing steps to avoid duplicate test scripts. If you are on the Home page and want to write a test automation script for a functionality using Page Object Model (POM), then all you need to do is create an object for the HomePage class & call the methods.

If the page does not have the method you are looking for, then create the method in the Home page class. So that, the other team members may reuse it for future script development.

In simple terms, you can use the Page Object Pattern to represent a GUI page into a class to improve readability and maintainability.

page object model

In the above diagram, Username, Password, and Login buttons are UI elements. The three elements are defined in the LoginPage class as WebElement and the required functional method (login) is also captured.

Benefits

Script Maintenance Page Object Model eases script maintenance. Let’s say you are automating test scripts using Java and JVM-Cucumber. All the assertions happen in Step Definitions files. If the username and password locators are changed, then you need to update the Login page class and not the step definition files.

Readability If the scripts are readable, then anyone in the team can reuse them. In the below screenshot, the script navigates to the eGift card page on Amazon.com. Only the Gherkin and the method name in the step definition gives some hint about the snippet.

page object model pattern

If the steps are published in POM you will get a readable script. Refer to the below snippet. The step definition calls a method from the HomePage object. Now, the script is more readable and maintainable.

page object model python

Decreases effort to create new tests Once the automation testing codebase is large and mature enough, you can reuse it for new script development. Adding automation test scripts quickly in the regression test suite will add more value to the project and product. Let’s say you have all the required pages and methods for your automation script development, then you need to call the methods to complete the scripting. As a test automation company, we store the Web App POM in pages package/folder and the Mobile App POM in Screens package/folder.

Abstraction Layer Page Object Model enables an abstraction layer between a step definition and page. When an automation tester creates a script, he/she will use the IDE’s IntelliSense to view the methods which are associated with an object before selecting a method.

page object model best practices

Common Mistakes while implementing Page Object Model

Duplications Most modern Websites/Web Applications contain some common UI elements which are displayed on all the pages. Bread Crumb, Navigation menus, Footer Links, and Search area are all examples. If the common UI elements and validations are not captured on the common page, then your team will create duplicate codes for common objects on multiple pages.

To avoid duplication, create a page and use it for publishing common elements and validations. After that, inherit the common/base page in all other pages.

Adding Too Much If the UI page has multiple sections or components, then don’t capture all the UI elements and validations in one page. In simple terms, if the page object has more code, it will increase the maintenance.

Create classes for different components instead of writing everything in a single class.

Multi-Level Inheritance Don’t use multi-level inheritance as it will lead to confusion. It is always vital to remember that overdoing anything will never add value. An automation tester should be able to understand the code hierarchy quickly. So that, the automated test scripts can be updated/fixed easily. As a leading test automation service provider, we try to keep the framework simple enough for all the test automation projects.

Adding Assertion in Pages Don’t add assertions in pages. The reason is that pages are not meant for tests. A page method needs to perform actions on UI and return the result. Write assertions steps in step definitions/tests and not in pages.

Let’s say Tester 1 adds an assertion in a page method. Tester 2 may need the method only for performing actions and not for assertions because the assertion is unnecessary for Tester 2. Adding assertions in the POM decreases the chances of reusability.

Naming Convention Method name should reveal the purpose of the method. If a tester understands the purpose only after going through the method’s comment, then it is clear that the method doesn’t have a proper name.

Script Review

To create a robust test automation suite, you need highly skilled resources and the team needs to follow the best practices. Ensuring whether the team is following the automation best practices or not is the catch here.

Make sure not to add any page in the test suite without reviewing. Script reviewing sessions will ensure the standards and best practices.

Dependency Injection

In JVM-Cucumber, you can inject Constructor level Page objects. Before taking a deeper dive, let’s see what ‘Dependency Injection’ is. Dependency Injection is a service. Once an object is created, Dependency Injection will serve the object wherever you need it.

Let’s say you have two ‘step definition’ files and a Gherkin scenario shares the ‘steps implementation’ in those two files, then you need shared page objects. For example, you can’t have two different objects for the HomePage class, because you need to share the state of the object between steps.

If you use Picocontainer in JVM-Cucumber, it will inject the dependencies in a step definition’s constructor and the injected object state is shared wherever it is used. Please note – Object State sharing is only for steps and not for scenarios. If a scenario’s execution is complete, then the new instances of the step definitions will be created for the next scenario.

If you don’t use Dependency Injection, you will create static variables to manage object states. However, it will lead to unnecessary confusion and trouble-shooting failures will become a cumbersome task.

Dependency Injection using Picocontainer

picocontainer example

Refer to the above diagram. There are two ‘step definition’ files which are referring a static page object. However, the object state is continuing for Scenario 2 execution as well. This needs to be avoided. If the state is shared within a scenario, we can avoid data leaks.

cucumber picocontainer maven

Picocontainer serves the objects which are required for a step definition file. In the above diagram, the state is scoped at the scenario level. For each scenario execution, new objects will be created by the Picocontainer Dependency injector and the state will be shared within the steps.

You might have used Picocontainer Constructor Dependency Injection. In the following section, you will learn how to implement Annotated Field Injection.

Step-1 Create a maven project

how to put cucumber picocontainer on the classpath

Step-2 Create a feature file under test->resources. File name – ‘Amazon-eGift-Card.feature’

Feature: Amazon eGift Card Feature

  Scenario: As a customer, I see Newest eGift cards
    Given I am on eGift card page
    When I select Newest Arrivals sorting option
    Then I should able to see the Newest eGift card arrivals

Step-3 Create a Step Definition file under test->java. File Name – MyStepdefs

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.picocontainer.annotations.Inject;
import pages.HomePage;
import pages.EGiftCardPage;
import pages.ProductPage;
import utils.SharedDriver;

import java.util.HashMap;

import static org.junit.Assert.assertEquals;

public class MyStepdefs {

    @Inject private SharedDriver driver;
    @Inject HomePage homePage;
    @Inject ProductPage productPage;
    @Inject EGiftCardPage eGiftCardPage;

    @Given("^I am on eGift card page$")
    public void iAmOnEGiftCardPage() {
        homePage.goToEGiftCardPage();
    }

    @When("I select Newest Arrivals sorting option")
    public void iSelectNewestArrivalsSortingOption() {
        eGiftCardPage.selectNewestArrivals();
    }

    @Then("I should able to see the Newest eGift card arrivals")
    public void iShouldAbleToSeeTheNewestEGiftCardArrivals() {
        eGiftCardPage.selectFirstProduct();
        HashMap<String,String> productDetails =  productPage.getProductDetails();

        assertEquals(productDetails.get("MODEL_NUMBER"), "307_US_Email");

    }

}

Step-4 Create three pages under src->main->java->pages.

package pages;

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

public class HomePage {
    private final WebDriver driver;

    public HomePage(WebDriver driver){
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public void goToEGiftCardPage(){
        driver.get("https://www.amazon.com/");
        driver.findElement(By.id("nav-hamburger-menu")).click();
        driver.findElement(By.xpath("//div/parent::a[.='Gift Cards']")).click();
        driver.findElement(By.linkText("eGift cards")).click();
    }

}
package pages;

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

public class EGiftCardPage {
    private final WebDriver driver;

    public EGiftCardPage(WebDriver driver){
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public void selectNewestArrivals(){
        driver.findElement(By.id("a-autoid-0-announce")).click();
        driver.findElement(By.xpath("//a[.='Newest Arrivals']")).click();
    }

    public void selectFirstProduct(){
        driver.findElement(By.xpath("//span[@data-component-type='s-product-image']/a")).click();
    }
}

package pages;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

import java.util.HashMap;

public class ProductPage {
    private final WebDriver driver;

    @FindBy(xpath = "//div[@id='detailBullets_feature_div']//span[contains(.,'Item model number')]/following-sibling::span")
    private WebElement itemModelNumber;

    @FindBy(xpath = "//div[@id='detailBullets_feature_div']//span[contains(.,'ASIN')]/following-sibling::span")
    private WebElement ASIN;

    public ProductPage(WebDriver driver){
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public HashMap<String,String> getProductDetails(){
        HashMap<String, String> productDetails = new HashMap<String, String>();

        productDetails.put("MODEL_NUMBER", itemModelNumber.getText());
        productDetails.put("ASIN", ASIN.getText());

        return productDetails;
    }
}

Step-5 Create SharedDriver.java under src->main->java->utils

package utils;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.events.EventFiringWebDriver;

import java.util.concurrent.TimeUnit;

public class SharedDriver extends EventFiringWebDriver {
    private static final WebDriver driver;

    private static final Thread CLOSE_THREAD = new Thread() {
        @Override
        public void run() {
            driver.quit();
        }
    };

    static{
        System.setProperty("webdriver.chrome.driver", "drivers/chromedriver");
        driver = new ChromeDriver();
        driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
        Runtime.getRuntime().addShutdownHook(CLOSE_THREAD);
    }


    public SharedDriver() {
        super(driver);
    }
}

Step-6 Create PicoDependencyInjector.java under src->main->java->utils. If you see the PicoDependencyInjector file, the required pages are added in the injector.


package utils;


import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.picocontainer.PicoFactory;
import pages.EGiftCardPage;
import pages.HomePage;
import pages.ProductPage;

public class PicoDependencyInjector implements ObjectFactory {

    private PicoFactory delegate = new PicoFactory();

    public PicoDependencyInjector() {
        addClass(SharedDriver.class);
        addClass(HomePage.class);
        addClass(EGiftCardPage.class);
        addClass(ProductPage.class);
    }

    @Override
    public void start() {
        delegate.start();
    }

    @Override
    public void stop() {
        delegate.stop();
    }

    @Override
    public boolean addClass(Class<?> aClass) {
        return delegate.addClass(aClass);
    }

    @Override
    public <T> T getInstance(Class<T> aClass) {
        return delegate.getInstance(aClass);
    }
}

Step-7 Now, you need to notify Cucumber that you are using custom injector. All you need to do is create a ‘io.cucumber.core.backend.ObjectFactory’ file in src->main->java->resources->META-INF->services and mention the ‘utils.PicoDependencyInjector’ class.

cucumber reuse step definitions java

Step-8 Create cucumber.properties in test->resources and add the below content.

cucumber.object-factory=utils.PicoDependencyInjector

Step-9 Add the below listed maven dependencies in POM.xml

<dependencies>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-picocontainer</artifactId>
            <version>6.9.1</version>
        </dependency>

        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>6.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>4.0.0-alpha-5</version>
        </dependency>

        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>6.9.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Viola, you’re done! If you refer to the step definition file, the objects are created using @Inject annotation at the field level. You can view the full code in the following repository – cucumber-di-example

Conclusion

As a QA company, we rely on automation testing to provide testing feedback quickly to our clients’ teams. Following best practices and design patterns improve readability and maintainability. If adding new scripts is taking time, then you don’t have reusable scripts or you have just started implementing automation testing for your project. We hope this article has bought to light the full potential of using the Page Object Model by explaining how you can use it effectively.

Comments(2)

  • 1 year ago

    Нello there, You've done a fantastic job. I'll definitely digg it and pers᧐nally suggest tⲟ my friends. I am cоnfident they will bе Ƅenefited from this site.

  • 1 year ago

    Way cool! Some eҳtгemely valid points! I appreciate you writing this article and tһе rest of the site is also rеally good.

Submit a Comment

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

Talk to our Experts

Amazing clients who
trust us


poloatto
ABB
polaris
ooredo
stryker
mobility