Page Object Pattern using PageFactory
Creating Selenium test cases can result in an un-maintainable project. One of the reasons is that too many duplicated code is used. Duplicated code could be caused by duplicated functionality and this will result in duplicated usage of locators. The disadvantage of duplicated code is that the project is less maintainable. If some locator will change, you have to walk through the whole test code to adjust locators where necessary. By using the Page Object Pattern using PageFactory we can make non-brittle test code and reduce or eliminate duplicate test code. Besides that it improves the readability and allows us to create interactive documentation. Last but not least, we can create tests with less keystroke. An implementation of the page object model can be achieved by separating the abstraction of the test object and the test scripts.
Advantages of using Page Object Pattern:
- Easy to Maintain
- Easy Readability of scripts
- Reduce or Eliminate duplicacy
- Re-usability of code
- Reliability
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: If you are new with Selenium and Java programming language, I would suggest you to carry on with this strategy and follow Page Factory once you got good command over your test scripts.
2) Page Factory class: Please follow the article.
Selenium PageFactory
The PageFactory Class in Selenium is an extension to the Page Object design pattern. It is used to initialize the elements of the Page Object or instantiate the Page Objects itself. Annotations for elements can also be created (and recommended) as the describing properties may not always be descriptive enough to tell one object from the other.
It 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. There is however a few differences between C# and Java implementation – Java provide greater flexibility with PageFactory.
How to do it…
We will follow a step by step instruction to set up Page Factory.
Step 1: Take an example of simple test case of LogIn application and understand where we can use Page factory.
Step 2: Implement Page Factory in the LogIn test case
Step 3: Use Annotations of Page Factory
Step 4: Divide the single Page Factory Objects in to different Page Classes
Step 5: Combine set of repetitive actions in to methods to implement Modularization.
Step 1: Test Case of LogIn application
- Launch a WebDriver
- Navigate to Url www.store.demoqa.com
- Click on My Account link
- Enter Username & Password
- Print a system message and Log out
- Close WebDriver
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | package automationFramework; import java.util.concurrent.TimeUnit; import org.openqa.selenium.*; import org.openqa.selenium.firefox.FirefoxDriver; public class LogIn_TestCase { private static WebDriver driver = null; public static void main(String[] args) { // Create a new instance of the Firefox driver driver = new FirefoxDriver(); //Put a Implicit wait, this means that any search for elements on the page could take the time the implicit wait is set for before throwing exception driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); //Launch the Online Store Website driver.get("https://www.store.demoqa.com"); // Find the element that's ID attribute is 'account'(My Account) driver.findElement(By.id("account")).click(); // Find the element that's ID attribute is 'log' (Username) // Enter Username on the element found by above desc. driver.findElement(By.id("log")).sendKeys("testuser_1"); // Find the element that's ID attribute is 'pwd' (Password) // Enter Password on the element found by the above desc. // Now submit the form. WebDriver will find the form for us from the element driver.findElement(By.id("login")).click(); // Print a Log In message to the screen System.out.println(" Login Successfully, now it is the time to Log Off buddy."); // Find the element that's ID attribute is 'account_logout' (Log Out) driver.findElement(By.id("account_logout")); // Close the driver driver.quit(); } } |
Step 2: Implement PageFactory in the LogIn test case
The PageFactory relies on using sensible defaults, means the name of the field in the Java class is assumed to be the “id” or “name” of the element on the HTML page. For example:
1 2 3 | userName.sendKeys(text); password.sendKeys(text); |
Is equivalent to:
1 2 3 | driver.findElement(By.id("userName")).sendKeys(text); driver.findElement(By.id("password")).sendKeys(text); |
Let’s see how it works in real world:
1. Create a ‘New Package’ file and name it as ‘automationFramework’, by right click on the Project and select New > Package.
2. Create a ‘New Class’ file and refer it as ‘TestCase_POF‘, by right click on the above created Package and select New > Class.
3. From the above test ‘LogIn_TestCase’, take the login functionality and implement PageFactory design.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package automationFramework; import java.util.concurrent.TimeUnit; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.support.PageFactory; public class TestCase_POF { static WebDriver driver; // In order to use the PageFactory, first declare some fields on a PageObject that are WebElements static WebElement log; static WebElement pwd; static WebElement submit; public static void main(String[] args) throws InterruptedException{ driver = new FirefoxDriver(); driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); driver.get("https://store.demoqa.com"); // In order for this code to work and not throw a NullPointerException because // the "log", "pwd" nd "submit" fields aren't instantiated, we need to initialise the PageObject PageFactory.initElements(driver, TestCase_POF.class); // Once Instantiated, we can now use the above created WebElements log.sendKeys("testuser_1"); submit.click(); driver.quit(); } } |
Note: When we run the example, the PageFactory will search for an element on the page that matches the field name of the ‘WebElement’ in the class. It does this by first looking for an element with a matching ‘ID’ attribute. If this fails, then it falls back to searching for an element by the value of its ‘NAME’ attribute.
Step 3: Use Annotations of PageFactory
Annotations in PageFactory are like this:
1 2 3 | @FindBy(how=How.ID,using="userName") private WebElement txtbx_UserName; |
This is used to mark a field on a Page Object to indicate an alternative mechanism for locating an element. Used in conjunction with PageFactory this allows users to quickly and easily create Page Objects. You can either use this annotation by specifying both “how” and “using” or by specifying one of the location strategies (eg: “id”) with an appropriate value to use. Both options will delegate down to the matching ‘By’ methods in By class.
For example, these two annotations point to the same element:
1 2 3 | @FindBy(id = “userName”) WebElement txt_UserName; |
1 2 3 | @FindBy(how = How.ID, using = “userName”) WebElement txt_UserName; |
Now using above technique, turn your test case in to new test case with annotations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | package automationFramework; import java.util.concurrent.TimeUnit; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.How; import org.openqa.selenium.support.PageFactory; public class TestCase_POF { static WebDriver driver; @FindBy(how = How.XPATH, using = ".//*[@id='account']/a") static WebElement lnk_MyAccount; @FindBy(how = How.ID, using = "log") static WebElement txtbx_UserName; @FindBy(how = How.ID, using = "pwd") static WebElement txtbx_Password; @FindBy(how = How.NAME, using = "submit") static WebElement btn_Login ; @FindBy(how = How.XPATH, using = ".//*[@id='account_logout']/a") static WebElement lnk_LogOut; public static void main(String[] args) throws InterruptedException{ driver = new FirefoxDriver(); driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); driver.get("https://www.store.demoqa.com"); PageFactory.initElements(driver, TestCase_POF.class); lnk_MyAccount.click(); txtbx_UserName.sendKeys("testuser_1"); btn_Login.click(); System.out.println(" Login Successfully, now it is the time to Log Off buddy."); lnk_LogOut.click(); driver.quit(); } } |
Note: ‘How‘ can be used with ‘ID’, ‘NAME’, ‘XPATH’ and many more.
Note: As told earlier that PageFactory Instantiates all the elements of the web page at the start when we Initialized any page class objects. But think of the elements which will display on the web page after some action, say Ajax action. In PageFactory, every time when we call a method on the WebElement, the driver will go and find it on the current page once again.
In an AJAX-heavy application this is what we would like to happen, but in the case of regular application where elements are stable and when we know that the element is always going to be there and won’t change. It would be handy if we could ‘cache’ the element once we’d looked it up and save some time of execution by commanding PageFactory to not search the WebElements on the page again.:
@CacheLookup
If we know that element is always present on the page, it is best to use the following declaration:
1 2 3 4 5 | @FindBy(how = How.ID, using = "userName") @CacheLookup private WebElement txt_UserName; |
If we don’t do it, then every time when we turn to our element, WebDriver will check if the element is present on the page.
Step 4: Divide the single PageFactory Objects in to different Page Classes
1. Create a ‘New Package’ file and name it as ‘pageObjects’, by right click on the Project and select New > Package.
2. Create ‘New Class’ file for Home Page & LogIn Page and refer the name to the actual page from the test object, by right click on the above created Package and select New > Class. In our case it is Home Page and LogIn Page.
Home Page Class
As My Account link and Log out link resides in the header of the webpage, I always consider a Header as a part of Home Page. As it remain same throughout the application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package pageObjects; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.How; public class Home_PG_POF { final WebDriver driver; @FindBy(how = How.XPATH, using = ".//*[@id='account']/a") public WebElement lnk_MyAccount; @FindBy(how = How.XPATH, using = ".//*[@id='account_logout']/a") public WebElement lnk_LogOut; // This is a constructor, as every page need a base driver to find web elements public Home_PG_POF(WebDriver driver) { this.driver = driver; } } |
LogIn Page Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package pageObjects; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.How; public class LogIn_PG_POF { final WebDriver driver; @FindBy(how = How.ID, using = "log") public WebElement txtbx_UserName; @FindBy(how = How.ID, using = "pwd") public WebElement txtbx_Password; @FindBy(how = How.NAME, using = "submit") public WebElement btn_Login ; // This is a constructor, as every page need a base driver to find web elements public LogIn_PG_POF(WebDriver driver){ this.driver = driver; } } |
Test Case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package automationFramework; import java.util.concurrent.TimeUnit; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.support.PageFactory; import pageObjects.Home_PG_POF; import pageObjects.LogIn_PG_POF; public class TestCase_POF { static WebDriver driver; public static void main(String[] args) throws InterruptedException{ driver = new FirefoxDriver(); driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); driver.get("https://www.store.demoqa.com"); // This is to Instantiate Home Page and LogIn Page class Home_PG_POF HomePage = PageFactory.initElements(driver, Home_PG_POF.class); LogIn_PG_POF LoginPage = PageFactory.initElements(driver, LogIn_PG_POF.class); // Once both classes Initialised, use their Web Element Objects HomePage.lnk_MyAccount.click(); LoginPage.txtbx_UserName.sendKeys("testuser_1"); LoginPage.btn_Login.click(); System.out.println(" Login Successfully, now it is the time to Log Off buddy."); HomePage.lnk_LogOut.click(); driver.quit(); } } |
Note: We have created an Instance of class ‘Home_PG_POF’ and named it as ‘HomePage’, in our test case, so that we can use its Objects. Once any class is Initialized, you can use all of its objects and method by typing its name like ‘HomePage’ and press dot, it will populate all the public methods and objects reside in it. Same is like LogIn Page.
Step 5: Combine set of repetitive actions in to methods to implement Modularization.
Imagine a scenario of multiple tests which require the use of this LogIn functionality . The same LogIn code will be repeated again and again in each test. Any change in UI will mean that all tests will have to be modified. Why not create a separate method and put series of actions in it like, entering Username & Password and click Submit button.
This method is for joining LogIn actions, so it is better to create it in LogIn Page class.
LogIn Page Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | package pageObjects; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.How; public class LogIn_PG_POF { final WebDriver driver; @FindBy(how = How.ID, using = "log") public WebElement txtbx_UserName; @FindBy(how = How.ID, using = "pwd") public WebElement txtbx_Password; @FindBy(how = How.NAME, using = "submit") public WebElement btn_Login ; public LogIn_PG_POF(WebDriver driver) { this.driver = driver; } // This method will take two arguments ( Username nd Password) public void LogIn_Action(String sUserName, String sPassword){ txtbx_UserName.sendKeys(sUserName); txtbx_Password.sendKeys(sPassword); btn_Login.click(); } } |
Home Page class will remain same, as there is no change in it.
As it is out final test, why not create a new class with TestNG. Our final test case will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | package automationFramework; import java.util.concurrent.TimeUnit; import pageObjects.Home_PG_POF; import pageObjects.LogIn_PG_POF; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.support.PageFactory; import org.testng.annotations.Test; import org.testng.annotations.BeforeMethod; import org.testng.annotations.AfterMethod; public class PageFactory_TestCase { static WebDriver driver; Home_PG_POF HomePage; LogIn_PG_POF LoginPage; @BeforeMethod public void beforeMethod() { driver = new FirefoxDriver(); driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); driver.get("https://www.store.demoqa.com"); HomePage = PageFactory.initElements(driver, Home_PG_POF.class); LoginPage = PageFactory.initElements(driver, LogIn_PG_POF.class); } @Test public void test() { HomePage.lnk_MyAccount.click(); System.out.println(" Login Successfully, now it is the time to Log Off buddy."); HomePage.lnk_LogOut.click(); } @AfterMethod public void afterMethod() { driver.quit(); } } |
A Page Object Model can be maintained in other way as well with regular java classes, please visit Page Object Model for that.