The following tutorial is the next step in Selenium Cucumber Framework series. In this chapter, we will learn more about Page Object Model Framework which is also known as Page Object Design Pattern or Page Objects. The main advantage of Page Object Model is that if the UI or any HTML object changes for any page, the test does not need any fix. Only the code within the page objects will be impacted but it does not have any impact to the test.
Page Object Design Pattern
Till now we have just one test and one step file of cucumber. Can you image the line the of code we will have in our project when we will deal with 100+ tests and that will have multiple stepDefiniions files. The whole project code will become unmanageable and unmaintainable. To better manage the code and to improve the re-usability, this pattern suggests us to divided an application in different pages or a single page into sub pages. So far we were writing code with no actual structure, focusing only on elements and sending commands to Selenium driver.
Hence, the Page Object Pattern technique provides a solution for working with multiple web pages and prevents unwanted code duplication and enables an uncomplicated solution for code maintenance. In general, every page in our application will be represented by a unique class of its own and the page element inspection will be implemented in every class.
How to implement the Page Object Model?
There are two different ways of implementing POM:
1) Regular Java classes: Please visit Page Object Model.
Note: This is not recommended, as Selenium has offer us a better solution for this, which is Selenium PageFactory.
- Selenium PageFactory : Page Factory is an inbuilt Page Object Model concept for Selenium WebDriver and it is very optimized. To learn more on Page Factory, please visit our tutorial Page Object Pattern using Selenium PageFactory.
PageFactory is used to Initialize Elements of a Page class without having to use ‘FindElement‘ or ‘FindElements‘. Annotations can be used to supply descriptive names of target objects to improve code readability.
@FindBy Annotation
As the name suggest, it helps to find the elements in the page using By strategy. @FindBy can accept TagName, PartialLinkText, Name, LinkText, Id, Css, ClassName, XPath as attributes. An alternative mechanism for locating the element or a list of elements. This allows users to quickly and easily create PageObjects.
@FindBy(how = How.CSS, using = “.username“)]
private WebElement userName;
The above code will create a PageObject and name it as UserName by finding it using its CSS locator.
InitElements
This Instantiate an Instance of the given class. This method will attempt to instantiate the class given to it, preferably using a constructor that takes a WebDriver instance as its only argument or falling back on a no-arg constructor. An exception will be thrown if the class cannot be instantiated.
PageFactory.initElements(WebDriver, PageObject.Class);
Parameters:
- WebDriver – The driver that will be used to look up the elements
- PageObjects – A class which will be initialised
Returns: An instantiated instance of the class with WebElement and List<WebElement>
fields proxied
PageFactory NameSpace
PageFactory functionality resides in import org.openqa.selenium.support.PageFactory;
Page Object Design Pattern with Selenium PageFactory in Cucumber
In the last chapter of Convert Selenium Test in to Cucumber, we chose one End 2 End test Case to automate. Let’s see how Page Object Design Pattern with Selenium PageFactory in Cucumber works in real world.
If I asked you to divide our test page objects into different pages, what would be your solution. Let see how I divided the same:
- Home Page
- Product Listing Page
- Cart Page
- Checkout Page
- Confirmation Page
Our test pass through these above pages, so it makes sense to divide the application into these pages. Maybe later you further like to break down the Checkout Page into sub-pages like:
- Personal Details Page
- Shipping Details Page
- Payment Details Page
But so far we do not this much of code that our checkout page class would look complicated. Let's just stick with the 5 pages for now.
Create Checkout Page Object Class
-
Create a New Package file and name it as pageObjects, by right click on the src/main/java and select New >> Package. As page objects are the part of the framework, we keep all the page objects in src/main/java folder.
-
Create a New Class file and name it as the actual page from the test object, by right click on the above-created Package and select New >> Class. In our case it is CheckoutPage.
CheckoutPage.class
package pageObjects;
import java.util.List;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.support.PageFactory;
public class CheckoutPage {
public CheckoutPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(how = How.CSS, using = "#billing_first_name")
private WebElement txtbx_FirstName;
@FindBy(how = How.CSS, using = "#billing_last_name")
private WebElement txtbx_LastName;
@FindBy(how = How.CSS, using = "#billing_email")
private WebElement txtbx_Email;
@FindBy(how = How.CSS, using = "#billing_phone")
private WebElement txtbx_Phone;
@FindBy(how = How.CSS, using = "#billing_country_field .select2-arrow")
private WebElement drpdwn_CountryDropDownArrow;
@FindBy(how = How.CSS, using = "#billing_state_field .select2-arrow")
private WebElement drpdwn_CountyDropDownArrow;
@FindAll(@FindBy(how = How.CSS, using = "#select2-drop ul li"))
private List<WebElement> country_List;
@FindBy(how = How.CSS, using = "#billing_city")
private WebElement txtbx_City;
@FindBy(how = How.CSS, using = "#billing_address_1")
private WebElement txtbx_Address;
@FindBy(how = How.CSS, using = "#billing_postcode")
private WebElement txtbx_PostCode;
@FindBy(how = How.CSS, using = "#ship-to-different-address-checkbox")
private WebElement chkbx_ShipToDifferetAddress;
@FindAll(@FindBy(how = How.CSS, using = "ul.wc_payment_methods li"))
private List<WebElement> paymentMethod_List;
@FindBy(how = How.CSS, using = "#terms.input-checkbox")
private WebElement chkbx_AcceptTermsAndCondition;
@FindBy(how = How.CSS, using = "#place_order")
private WebElement btn_PlaceOrder;
public void enter_Name(String name) {
txtbx_FirstName.sendKeys(name);
}
public void enter_LastName(String lastName) {
txtbx_LastName.sendKeys(lastName);
}
public void enter_Email(String email) {
txtbx_Email.sendKeys(email);
}
public void enter_Phone(String phone) {
txtbx_Phone.sendKeys(phone);
}
public void enter_City(String city) {
txtbx_City.sendKeys(city);
}
public void enter_Address(String address) {
txtbx_Address.sendKeys(address);
}
public void enter_PostCode(String postCode) {
txtbx_PostCode.sendKeys(postCode);
}
public void check_ShipToDifferentAddress(boolean value) {
if(!value) chkbx_ShipToDifferetAddress.click();
try { Thread.sleep(3000);}
catch (InterruptedException e) {}
}
public void select_Country(String countryName) {
drpdwn_CountryDropDownArrow.click();
try { Thread.sleep(2000);}
catch (InterruptedException e) {}
for(WebElement country : country_List){
if(country.getText().equals(countryName)) {
country.click();
try { Thread.sleep(3000);}
catch (InterruptedException e) {}
break;
}
}
}
public void select_County(String countyName) {
drpdwn_CountyDropDownArrow.click();
try { Thread.sleep(2000);}
catch (InterruptedException e) {}
for(WebElement county : country_List){
if(county.getText().equals(countyName)) {
county.click();
try { Thread.sleep(3000);}
catch (InterruptedException e) {}
break;
}
}
}
public void select_PaymentMethod(String paymentMethod) {
if(paymentMethod.equals("CheckPayment")) {
paymentMethod_List.get(0).click();
}else if(paymentMethod.equals("Cash")) {
paymentMethod_List.get(1).click();
}else {
new Exception("Payment Method not recognised : " + paymentMethod);
}
try { Thread.sleep(3000);}
catch (InterruptedException e) {}
}
public void check_TermsAndCondition(boolean value) {
if(value) chkbx_AcceptTermsAndCondition.click();
}
public void clickOn_PlaceOrder() {
btn_PlaceOrder.submit();
}
public void fill_PersonalDetails() {
enter_Name("Aotomation");
enter_LastName("Test");
enter_Phone("0000000000");
enter_Email("[email protected]");
select_Country("India");
enter_City("Delhi");
enter_Address("Shalimar Bagh");
enter_PostCode("110088");
select_County("Delhi");
}
}
Code Explanation
1) Constructor
As mentioned above, to initiate page object factory the code is :
CheckoutPage checkoutPage = new CheckoutPage();
PageFactory.initElements(driver, checkoutPage );
But the duty of the Framework is to minimize the code where ever it can. Let say if the application has ten pages to automate. To do the task, it is required to create the object of ten pages first and then initialize ten pages to use their objects. This will make the test very lengthy and filthy. It is a good idea to abstract this task from the test. By placing the Init statement in the Page Class Constructor, half of the code from the test can be reduced.
public CheckoutPage(WebDriver driver) { PageFactory.initElements(driver, this); }
2) Binding Methods
The other way to Optimizing Page Object Model or improving the test code is to wrap re-usable actions in the PageObject class itself. Now think of the reusable function in the example, that we have used in this Selenium Cucumber Framework series. The answer is entering personal details. The code for entering personal details will be called again and again in every test script. Isn’t a nice idea to wrap that action into a function and use it as many time as required.
Doing this is not a difficult task, as above we already initialized the elements of the page within the class constructor. Now the elements are ready to use in the PageObject class itself.
public void fill_PersonalDetails() {
enter_Name("Aotomation");
enter_LastName("Test");
enter_Phone("0000000000");
enter_Email("[email protected]");
select_Country("India");
enter_City("Delhi");
enter_Address("Shalimar Bagh");
enter_PostCode("110088");
select_County("Delhi");
}
Create other Page Objects for the test
Now likewise create all the page objects for other pages as well.
HomePage.java
package pageObjects;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.PageFactory;
public class HomePage {
WebDriver driver;
public HomePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void perform_Search(String search) {
driver.navigate().to("https://shop.demoqa.com/?s=" + search + "&post_type=product");
}
public void navigateTo_HomePage() {
driver.get("https://www.shop.demoqa.com");
}
}
ProductListingPage.java
package pageObjects;
import java.util.List;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.support.PageFactory;
public class ProductListingPage {
public ProductListingPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(how = How.CSS, using = "button.single_add_to_cart_button")
private WebElement btn_AddToCart;
@FindAll(@FindBy(how = How.CSS, using = ".noo-product-inner"))
private List<WebElement> prd_List;
public void clickOn_AddToCart() {
btn_AddToCart.click();
}
public void select_Product(int productNumber) {
prd_List.get(productNumber).click();
}
}
CartPage.java
package pageObjects;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.support.PageFactory;
public class CartPage {
public CartPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(how = How.CSS, using = ".cart-button")
private WebElement btn_Cart;
@FindBy(how = How.CSS, using = ".checkout-button.alt")
private WebElement btn_ContinueToCheckout;
public void clickOn_Cart() {
btn_Cart.click();
}
public void clickOn_ContinueToCheckout(){
btn_ContinueToCheckout.click();
try { Thread.sleep(5000);}
catch (InterruptedException e) {}
}
}
Usage of Page Objects in Step Definition file of Cucumber
package stepDefinitions;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
public class Steps {
WebDriver driver;
@Given("^user is on Home Page$")
public void user_is_on_Home_Page(){
System.setProperty("webdriver.chrome.driver","C:\\ToolsQA\\Libs\\Drivers\\chromedriver.exe");
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.get("https://www.shop.demoqa.com");
}
@When("^he search for \"([^\"]*)\"$")
public void he_search_for(String product) {
HomePage home = new HomePage(driver);
home.perform_Search(product);
}
@When("^choose to buy the first item$")
public void choose_to_buy_the_first_item() {
ProductListingPage productListingPage = new ProductListingPage(driver);
productListingPage.select_Product(0);
productListingPage.clickOn_AddToCart();
}
@When("^moves to checkout from mini cart$")
public void moves_to_checkout_from_mini_cart(){
CartPage cartPage = new CartPage(driver);
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
@When("^enter personal details on checkout page$")
public void enter_personal_details_on_checkout_page() throws InterruptedException {
CheckoutPage checkoutPage = new CheckoutPage(driver);
checkoutPage.fill_PersonalDetails();
}
@When("^select same delivery address$")
public void select_same_delivery_address() throws InterruptedException{
CheckoutPage checkoutPage = new CheckoutPage(driver);
checkoutPage.check_ShipToDifferentAddress(false);
}
@When("^select payment method as \"([^\"]*)\" payment$")
public void select_payment_method_as_payment(String arg1){
CheckoutPage checkoutPage = new CheckoutPage(driver);
checkoutPage.select_PaymentMethod("CheckPayment");
}
@When("^place the order$")
public void place_the_order() throws InterruptedException {
CheckoutPage checkoutPage = new CheckoutPage(driver);
checkoutPage.check_TermsAndCondition(true);
checkoutPage.clickOn_PlaceOrder();
driver.quit();
}
}
I hope it is easy to understand and does not require any explanation. We just brought page object concept in to the step definition file and called their methods.
Improve Code Re-Usability of Step Definition File
If you notice that in the step file, we created Checkout Page object almost four different times for four different methods. This is because the scope of Local Variable in Java is limited to its method only. To improve that, it is better to create Class Variables like
public class Steps {
HomePage home;
ProductListingPage productListingPage;
CartPage cartPage;
CheckoutPage checkoutPage;
}
With that your Step Definition file will look this now:
Steps.java
package stepDefinitions;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
public class Steps {
WebDriver driver;
HomePage home;
ProductListingPage productListingPage;
CartPage cartPage;
CheckoutPage checkoutPage;
@Given("^user is on Home Page$")
public void user_is_on_Home_Page(){
System.setProperty("webdriver.chrome.driver","C:\\ToolsQA\\Libs\\Drivers\\chromedriver.exe");
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.get("https://www.shop.demoqa.com");
}
@When("^he search for \"([^\"]*)\"$")
public void he_search_for(String product) {
home = new HomePage(driver);
home.perform_Search(product);
}
@When("^choose to buy the first item$")
public void choose_to_buy_the_first_item() {
productListingPage = new ProductListingPage(driver);
productListingPage.select_Product(0);
productListingPage.clickOn_AddToCart();
}
@When("^moves to checkout from mini cart$")
public void moves_to_checkout_from_mini_cart(){
cartPage = new CartPage(driver);
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
@When("^enter personal details on checkout page$")
public void enter_personal_details_on_checkout_page() throws InterruptedException {
checkoutPage = new CheckoutPage(driver);
checkoutPage.fill_PersonalDetails();
}
@When("^select same delivery address$")
public void select_same_delivery_address() throws InterruptedException{
checkoutPage.check_ShipToDifferentAddress(false);
}
@When("^select payment method as \"([^\"]*)\" payment$")
public void select_payment_method_as_payment(String arg1){
checkoutPage.select_PaymentMethod("CheckPayment");
}
@When("^place the order$")
public void place_the_order() throws InterruptedException {
checkoutPage.check_TermsAndCondition(true);
checkoutPage.clickOn_PlaceOrder();
driver.quit();
}
}
Project Explorer