Selenium PageObjects and PageFactory

PageObject Design Pattern

The PageObject design pattern models areas of a UI as objects within test code. The functionality classes (PageObjects) in this design represent a logical relationship between the pages of the application. Each class is referred to as a PageObject and returns other PageObjects to facilitate the flow between pages. Because PageObjects are returned, it becomes necessary to model both successful and unsuccessful events that can occur when interacted with a page.

For example, consider logging into Gmail. After entering the user details, the step either passes and navigates to the Inbox page or stays on the Login page possibly due to invalid input parameters. A pass would then return the Inbox PageObject whereas a fail would return the Login PageObject.

This means better tests, exception handling and reporting. It may sound a little confusing, but its quite a simple yet an elegant approach to write your tests. Let’s break down the above explanation into actual PageObjects.

In the code below, the following events occur:

  1. Constructor verifies if page is valid

  2. Attempts login to the Flights application

  3. (If successful) Returns the FindFlights PageObject

class LoginPage{
    private IWebDriver driver;     public LoginPage(IWebDriver driver)
    {
        this.driver = driver;         // 1. verify if page is valid
        if (driver.Title != "Welcome: Mercury Tours")
            throw new NoSuchWindowException("This is not the Login page");
    }     // return FindFlightsPage PageObject
    public FindFlightsPage Do(string UserName, string Password)
    {
        // 2. steps to login to the Flights application
        driver.FindElement(By.Name("userName")).SendKeys(UserName);
        driver.FindElement(By.Name("password")).SendKeys(Password);
        driver.FindElement(By.Name("login")).Click();         // 3. return FindFlights PageObject
        return new FindFlightsPage(driver);
    }}

For the above to work, the FindFlightsPage PageObject must be created. In the code below, the FindFlightsPage PageObject is modeled to perform the following actions:

  1. Constructor verifies if page is valid

  2. A method is called to find a flight

  3. The Logout method is called and actions performed

  4. Return the LoginPage PageObject

class FindFlightsPage{
    private IWebDriver driver;     public FindFlightsPage(IWebDriver driver)
    { 
        this.driver = driver;         // 1. verify if page is valid
        if (driver.Title != "Find a Flight: Mercury Tours:")
            throw new NoSuchWindowException("This is not the FindFlights page");
    }     // 2. method/code-block to find a flight
    public void Do()
    {
        Console.WriteLine("In FindFlightsPage.Do [Checking for Flights]");
    }     // returns LoginPage PageObject
    public LoginPage Logout()
    {
        // 3. log-off and return to LoginPage
        driver.FindElement(By.LinkText("SIGN-OFF")).Click();
        driver.FindElement(By.LinkText("Home")).Click();         // 4. return the LoginPage object
        return new LoginPage(driver);
    }}

Main execution entry point (the test code) below.

using OpenQA.Selenium;using OpenQA.Selenium.Firefox;using OpenQA.Selenium.Support.UI;using System; class Program{
    static void Main()
    {
        // instantiate FirefoxDriver and navigate to NewTours flight app
        IWebDriver driver = new FirefoxDriver();         // navigate to NewTours app
        driver.Navigate().GoToUrl("http://newtours.demoaut.com");         // instantiate LoginPage
        LoginPage Login = new LoginPage(driver);                 // Login.Do returns the FindFlightsPage PageObject
        FindFlightsPage FindFlights = Login.Do("test", "test");         if (FindFlights != null) 
        { 
            // perform steps to find a flight
            FindFlights.Do();              // FindFlights.Logout returns LoginPage
            Login = FindFlights.Logout(); 
        } 
        Console.ReadLine();
        driver.Quit();
    }}

As we saw above, the Login.Do(args) method returns the FindFlights PageObject whereas theFindFlights.Logout() method returns the LoginPage PageObject. We saw that the public methods of each class represent the functionality offered by the page. The real-world application of this concept will certainly contain more actions against the UI and may return a large number of PageObjects.

PageFactory Class

The PageFactory Class is an extension to the PageObject design pattern. It is used to initialize the elements of the PageObject or instantiate the PageObject itself (not in C# though – see the Notes section below). 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.

The InitElements method of PageFactory initializes the elements of the PageObject. The code below shows PageFactory usage in detail.

using OpenQA.Selenium;using OpenQA.Selenium.Firefox;using OpenQA.Selenium.Support.PageObjects; // *using System; class LoginPage{
    private IWebDriver driver;     [FindsBy(How = How.Name)]
    private IWebElement userName; // How.NAME = userName     [FindsBy(How = How.Name)]
    private IWebElement password; // How.NAME = password     [FindsBy(How = How.Name)]
    private IWebElement login; // How.NAME = login     public LoginPage(IWebDriver driver)
    {
        this.driver = driver;
    }     public FindFlightsPage Do(string UserName, string Password)
    {    
        userName.SendKeys(UserName);
        password.SendKeys(Password);
        login.Click(); 
        PageFactory.InitElements(driver, (new FindFlightsPage(this.driver)));
        return new FindFlightsPage(driver);
    }} class Program{
    static void Main()
    {
        IWebDriver driver = new FirefoxDriver();
        driver.Navigate().GoToUrl("http://newtours.demoaut.com"); 
        LoginPage Login = new LoginPage(driver);         // initialize elements of the LoginPage class
        PageFactory.InitElements(driver, Login);
        // all elements in the 'WebElements' region are now alive!
        // FindElement or FindElements no longer required to locate elements 
        FindFlightsPage FindFlights = Login.Do("User", "Pass");
        driver.Quit();
    }}

The WebElements userNamepassword and login are not explicitly defined using property-value pairs. However, if you execute the code above, ‘UserName’ and ‘Password’ strings will be supplied to the relevant text fields. The WebElement variable names were enough to identify the controls.

In the above example, PageFactory.InitElements facilitates searching for elements marked with the FindsByattribute by using the NAME property (notice: How = How.Name) to find the target element. There are other ways of object identification though and it is not required to use the object property as the variable name to identify it (as shown next).

The How parameter of FindsBy attribute is used for the object property (html tag). Using= then defines the corresponding value of the How= parameter.

Until now, NAME property has been directly used as the variable name. This is not very flexible approach, and I was only using it to provide a quick overview. Annotations are possible, too. The WebElements can be defined by any descriptive name. In the code below, userName, password and login have been modified to txtUserName, txtPassword and txtLogin respectively.

class LoginPage{
    private IWebDriver driver;     [FindsBy(How = How.XPath, Using = "//input[@type='text' and @name='userName']")]
    private IWebElement txtUserName;     [FindsBy(How = How.Name, Using = "userName")]
    private IWebElement txtPassword;     [FindsBy(How = How.Name, Using = "login")]
    private IWebElement btnLogin;     public LoginPage(IWebDriver driver)
    {
        this.driver = driver;
    }     public FindFlightsPage Do(string UserName, string Password)
    {
        txtUserName.SendKeys(UserName);
        txtPasswowrd.SendKeys(Password);
        btnLogin.Click();         return new FindFlightsPage(driver);
    }}

In summary, PageFactory class can be used to initialize elements of a Page class without having to useFindElement or FindElements. Annotations can be used to supply descriptive names of target objects in the AUT to improve code readability. There are however a few differences between C# and Java implementation – Java provides greater flexibility with PageFactory (see Notes).

CacheLookup

One last thing that remains with PageFactory is the CacheLookupAttribute. This is important because it can be used to instruct the InitElements method to cache the element once its located. In other words, any attribute marked [CacheLookup] will not be searched over and over again – this is especially useful for elements that are always going to be there (not always true for AJAX apps). So, we can search once and cache. All elements used in this article can be defined by this declarative tag as they are static and are always present. Our LoginPage class then becomes:

class LoginPage{
    private IWebDriver driver;     [FindsBy(How = How.Name)][CacheLookup]
    private IWebElement userName;     [FindsBy(How = How.Name)][CacheLookup]
    private IWebElement password;      [FindsBy(How = How.Name)][CacheLookup]
    private IWebElement login;      public LoginPage(IWebDriver driver)
    {
        this.driver = driver;
    }     public FindFlightsPage Do(string UserName, string Password)
    {
        userName.SendKeys(UserName);
        password.SendKeys(Password);
        login.Click();         return new FindFlightsPage(driver);
    }}

Notes – Differences between C# and Java Implementation

There are 3 discrepencies I found in the PageFactory documentation at Google Code between the Java and C# implementation.

The first discrepancy is that in Java, the PageFactory.InitElements can return the PageObject. In C#, this is not the case as InitElements returns void. View this image for a snapshot from Google Code documentation showing Java returning the PageObject.

For the 2nd discrepancy, let’s refer to the documentation:

… It [PageFactory] does this by first looking for an element with a matching ID attribute. If this fails, the PageFactory falls back to searching for an element by the value of its “name” attribute.

The above is not the case for C# – a NoSuchElementException is thrown. The PageFactory implentation for C# only searches for elements using the ID and does not locate the elements using the NAME property, unless How = How.Name is explicitly specified.

class LoginPage{
    private IWebDriver driver;     [FindsBy]
    private IWebElement userName;     public LoginPage(IWebDriver driver) { this.driver = driver; }     public void Do(string UserName, string Password)
    {
        // userName is the NAME property, not ID
        // element will not be located
        // will throw a NoSuchElementException
        userName.SendKeys(UserName); 
    }} class Program{
    static void Main()
    {
        IWebDriver driver = new FirefoxDriver();
        driver.Navigate().GoToUrl("http://newtours.demoaut.com"); 
        LoginPage Login = new LoginPage(driver);
        PageFactory.InitElements(driver, Login);
        Login.Do("theUserName", "thePassword"); 
        driver.Quit();
    }}

The 3rd discrepancy I found was in the initial part of the same document and noticed the same behavior when testing with Eclipse. The Java implementation can locate the element even without the FindsBy attribute – this isn’t the case for C#. View this image that shows this feature with Java. The below code fails to work for Gmail page for Passwd textBox since the [FindsBy] attribute is not specified.

class GmailLoginPage{
    private IWebDriver driver;     [FindsBy]
    private IWebElement Email;     // element will not initialize because [FindsBy] attribute is missing
    private IWebElement Passwd;     public GmailLoginPage(IWebDriver driver) { this.driver = driver; }     public void Do(string UserName, string Password)
    {
        Email.SendKeys(UserName);         // fail here - NullReferenceException
        Passwd.SendKeys(Password);
    }} class Program{
    static void Main()
    {
        IWebDriver driver = new FirefoxDriver();
        driver.Navigate().GoToUrl("http://gmail.com"); 
        GmailLoginPage GmailLogin = new GmailLoginPage(driver);
        PageFactory.InitElements(driver, GmailLogin);
        GmailLogin.Do("theUserName", "thePassword"); 
        driver.Quit();
    }}


你可能感兴趣的:(selenium,PageFactory,PageObjects)