Hi, In this tutorial, we look at the differences between POM and PageFactory in selenium and
an end to end example implementation of PageFactory design pattern of POM.
What is the usage of Page Object Model (POM) and PageFactory class?
- We can separate the test objects(web elements are the test objects) from the test scripts.
- Advantages:
- Code reusability, maintainability, repositoy of objects, better redability.
Page Object Model (POM)
- An OOPs test design pattern using page class.
- We organize each web page objects/elements in a class.
- Test scripts are maintained in separate class.
- Locators are defined using "By" (By is a selenium class)
- Example:
public By userNameLocator = By.xpath("//*[@id=\"txtUsername\"]");
PageFactory:
- PageFactory is a selenium class; It is an optimized approach to implement the Page Object Model using PageFactory class.
- Locators are defined using "@FindBy" annotation of PageFactory class.
- Example:
@FindBy(xpath = "//*[@id=\"txtUsername\"]")
public WebElement userNameLocator;
@FindBys({
@FindBy(id="xyz"),
@FindBy(id="pqr")
})
- We use initElements method to initialize the elements of the web page.
- Example: Using class
HRMLoginPageFactory hrmLoginPF =
PageFactory.initElements(driver, HRMLoginPageFactory.class);
- Example: Using a Constructor
public HRMLoginPageFactory(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
Now, let's begin with an example:
Scenarios:
Login to the HRM Application
Navigate to Directory page and check for the Search button presence.
Login to the HRM Application
Navigate to Directory page and check for the Search button presence.
Project structure:
Steps:
- Download and install cucumber plug-in in Eclipse from Market Place
- Download and install TestNG plug-in in Eclipse from Market Place
- Create a new Maven project (say : CucumberTestNGSeleniumMavenPageFactoryPattern)
- Add the following dependencies in pom.xml
- cucumber-java 7.1.0
- cucumber-testng 7.1.0
- selenium-java 4.3.0
- testng 7.1.0
- Add the following plug-ins in pom.xml
- maven-surefire-plugin 3.0.0-M7
- maven-compiler-plugin 3.10.1
- Create feature files in
- folder: src/test/resources
- HRMLogin.feature
- Directory.feature
- Write code for cucumber runner
- folder : src/test/java
- package : com.sadakar.testng.runner
- class : RunCucumberTest.java
- Write code for driver, hooks
- folder : src/test/java
- package: com.sadakar.common
- BasePage.java for driver
- Hooks.java for Before and After cucumber hooks
- Write code for page objects in Page Factory classes
In this phase, implement page objects in page factory classes for each of the UI page and refer them wherever required in step definitions. In a long term, if there are any changes in locators, we can just refer to the particular page factory class and update at once.
Using this approach we can reduce a lot of code redundancy.
Using this approach we can reduce a lot of code redundancy.
- folder : src/test/java
- package: com.sadakar.pageobjects
- HRMLoginPageFactory.java for HRMLogin page web elements,
- for instance locating username, password and login button
- DirectoryPageFactory.java for Directory page web element,
- for instance view directory tab link, search button
- Write code for step definitions or say glue code
- HRMLogin.java
- Directory.java
- Run the scenarios from mvn command line
- Navigate to the project folder
- Run the below command for two scenarios.
mvn test -Dcucumber.filter.tags="@LoginValidCredentials or @DirectoryTabIsSearchButtonDisplayed"
Tap on the image for best view: - Run the scenarios from TestNG in eclipse and report analysis
- Right click on the project and then Run as TestNG
- Report will be generated at
- test-output> emailable-report.html
- Tap on the image for best view:
- Run the project using testng.xml
- Right click on the project > Run as testng.xml
- Report is same as above
- Make sure testng.xml is added in pom.xml file as configuration
- Analysis of cucumber report
Install all the required jars in the project by defining the dependencies and or plug-ins.
HRMLogin.feature
Login feature file, in use DataTable approach for username and password.
Directory.feature
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>CucumberTestNGSeleniumMavenPageFactoryPattern</groupId>
<artifactId>CucumberTestNGSeleniumMavenPageFactoryPattern</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-java -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>7.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-java -->
<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-testng -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-testng</artifactId>
<version>7.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.testng/testng -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-compiler-plugin -->
<dependency>
<groupId>tech.grasshopper</groupId>
<artifactId>extentreports-cucumber7-adapter</artifactId>
<version>1.7.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.aventstack/extentreports -->
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>5.0.8</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
<systemPropertyVariables>
<extent.reporter.spark.start>true</extent.reporter.spark.start>
<extent.reporter.spark.out>test-output/SparkReport/Spark.html</extent.reporter.spark.out>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
Login feature file, in use DataTable approach for username and password.
@HRMLogin
Feature: Login to HRM Application
I want to use this template for HRM Login page
@LoginValidCredentials
Scenario: LoginValidCredentials
Given User login to HRM application with UserName and Password
| Admin | admin123 |
Take the Login feature scenario as Background so repeated code can be avoided for login.
It's a good practice to implement tests independent so login is taken separately and the directory is taken separately including login.
RunCucumberTest.java
It's a good practice to implement tests independent so login is taken separately and the directory is taken separately including login.
@Directory
Feature: Dashboard page
I want to use this template for my Directory Page
Background:
Given User login to HRM application with UserName and Password
| Admin | admin123 |
@DirectoryTabIsSearchButtonDisplayed
Scenario: DirectoryTabIsSearchButtonDisplayed
Then User is on Directory page
Then Is Search button displayed
> tags syntax has beeen changed over time, with 7.1, cucumber accepts or , and , not
> classpath.features finds the features in the project.
> glue identifies the code the project
> plugin is used to create cucumber report or extent-report (The adapter used is to support 4.3 selenium)
> RunCucumberTest -- always append Test at the end of cucumber runner class so the testng identifies the root for the scripts.
package com.sadakar.testng.runner;
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;
@CucumberOptions(
tags = "@LoginValidCredentials or @DirectoryTabIsSearchButtonDisplayed",
features = "classpath:features", glue = { "com.sadakar.common", "com.sadakar.stepdefinitions",
"com.sadakar.testng.runner" },
plugin = { "pretty", "json:target/cucumber-reports/cucumber.json",
"html:target/cucumber-reports/cucumberreport.html",
"com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:"},
monochrome = true)
public class RunCucumberTest extends AbstractTestNGCucumberTests {
}
> I would prefer to keep the initialization of driver in a BasePage class since at a later time, if the execution has to be done through a jar, I can keep the CLI(Command Line Interface)
Hooks.java
package com.sadakar.common;
importorg.openqa.selenium.WebDriver;
publicclassBasePage{
publicstatic WebDriver driver;
}
> Here Before is a cucumber hook, using it set the driver for chrome, access the app url and etc.
> After is also a cucumber hook and after each scenario the driver quits.
HRMLoginPageFactory.java
package com.sadakar.common;
importorg.openqa.selenium.chrome.ChromeDriver;
importio.cucumber.java.After;
importio.cucumber.java.Before;
publicclassHooksextends BasePage {
@Before//Cucumber Before Hook
publicvoidsetupDriver()throws InterruptedException {
System.setProperty("webdriver.chrome.driver","D:\\chromedriver.exe");
driver =new ChromeDriver();
driver.manage().window().maximize();
driver.get("https://opensource-demo.orangehrmlive.com/index.php/auth/login");
}
@After// Cucumber After hook
publicvoidquitDriver()throws Exception {
driver.quit();
}
}
> In this class, we store the web elements as objects and perform actions so that we can hide the implementation of actual test scripts.
> Every page factory class require a driver to find the elements and we activate the driver in the class constructor
> Every page factory class require a driver to find the elements and we activate the driver in the class constructor
> Using @FindBy annotation find the element of web page
> loginHRM is the user defined method in which the actual logic is written to login to the application.
This is separated out from the cucumber test script.
DirectoryPageFactory.java
package com.sadakar.pageobjects;
importorg.openqa.selenium.WebDriver;
importorg.openqa.selenium.WebElement;
importorg.openqa.selenium.support.FindBy;
publicclassHRMLoginPageFactory{
final WebDriver driver;
// Constructor, as every page needs a Webdriver to find elements
publicHRMLoginPageFactory(WebDriver driver){
this.driver= driver;
}
// Locators for User Name, Password and for Login button
// UserName
@FindBy(xpath ="//*[@id=\"txtUsername\"]")
public WebElement userNameLocator;
// Password
@FindBy(xpath ="//*[@id=\"txtPassword\"]")
public WebElement passwordLocator;
// Login button
@FindBy(id ="btnLogin")
public WebElement loginButtonLocator;
// Method that performs Login action using the web elements
publicvoidloginHRM(String uName, String pwd){
userNameLocator.sendKeys(uName);
passwordLocator.sendKeys(pwd);
loginButtonLocator.click();
}
}
> Code walkthrough is same as above factory class.
HRMLogin.java
package com.sadakar.pageobjects;
importorg.openqa.selenium.WebDriver;
importorg.openqa.selenium.WebElement;
importorg.openqa.selenium.support.FindBy;
publicclassDirectoryPageFactory{
final WebDriver driver;
// Constructor, as every page needs a Webdriver to find elements
publicDirectoryPageFactory(WebDriver driver){
this.driver= driver;
}
// Locators for directory tab,search button
// Directory tab locator
@FindBy(xpath ="//*[@id=\"menu_directory_viewDirectory\"]")
public WebElement viewDirectoryLinkLocator;
// Search button locator
@FindBy(xpath ="//*[@id=\"searchBtn\"]")
public WebElement searchButtonLocator;
// Method that performs click on Directory tab
publicvoidviewDirectoryTabNavigation(){
viewDirectoryLinkLocator.click();
}
// Method that checks the search button presence
publicbooleanverifySearchButtonPresense(){
return viewDirectoryLinkLocator.isDisplayed();
}
}
> Cucumber step definition for login page.
> In the feature file, the username and password is taken as data driven test.
> Define page factory class object hrmLoginPF and initiate the elements using initElements method
Directory.java
package com.sadakar.stepdefinitions;
importjava.time.Duration;
importjava.util.List;
importorg.openqa.selenium.support.PageFactory;
importcom.sadakar.common.BasePage;
importcom.sadakar.pageobjects.HRMLoginPageFactory;
importio.cucumber.java.en.Given;
publicclassHRMLoginextends BasePage {
HRMLoginPageFactory hrmLoginPF = PageFactory.initElements(driver, HRMLoginPageFactory.class);
@Given("User login to HRM application with UserName and Password")
publicvoidloginToHRMApp(io.cucumber.datatable.DataTable dataTable){
List<List<String>> cells = dataTable.cells();
hrmLoginPF.loginHRM(cells.get(0).get(0), cells.get(0).get(1));
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
}
}
> Code explanation is similar as above class
package com.sadakar.stepdefinitions;
importjava.time.Duration;
importorg.openqa.selenium.support.PageFactory;
importorg.testng.Assert;
importcom.sadakar.common.BasePage;
importcom.sadakar.pageobjects.DirectoryPageFactory;
importio.cucumber.java.en.Then;
publicclassDirectoryextends BasePage{
DirectoryPageFactory directoryPF = PageFactory.initElements(driver, DirectoryPageFactory.class);
@Then("User is on Directory page")
publicvoiddirectoryPage(){
// Navigating to Directory Page/tab
directoryPF.viewDirectoryTabNavigation();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
}
@Then("Is Search button displayed")
publicvoidisSearchButtonDisplayed(){
// Verifying Search button is displayed or not
boolean isSearhButtonDisplayedTrue = directoryPF.verifySearchButtonPresense();
Assert.assertTrue(isSearhButtonDisplayedTrue);
}
}
This article also has extent-reports included that is not discussed in steps.