GoogleHomeJigsaw

Automated Testing Using Page Objects and WebDriver

When writing your automated test scripts it is helpful to abstract your interface from the assertions. One helpful method to do this is the the Page Objects pattern. Essentially, your interface is mapped into a class, with each object field representing a UI element on your page. With all your locators in one place, you have a single repository to update if your UI ever changes.

Page Objects are a popular pattern when using Selenium RC, a great resource for guidance is the Selenium Page Object Pattern post on The Automated Tester’s blog. If you’d like some more quality reading on basic Page Object implementation take a look at the Page Objects page on the Selenium Google Code Wiki.

In Selenium 1.x Page Object implementation was simply a nice compliment to the tool, but outside the scope of the project itself. With Selenium2/WebDriver, you get the PageFactory class which takes your custom class, and gives you a usable page. [Note: At the time of writing, September 2010, the support package was only available in the Java library. Hopefully other languages will be supported by the 2.0 release]

WebDriver does this by a combination of clever conventions, and magic. Mostly magic. Following the example on the PageFactory wiki page mentioned above, I’ll demonstrate a quick test on the Bing home page. First you want your search page class. You want this class to support both the elements you’ll work with, and the services it provides. The minimum elements you’ll need to support on a search page are the search box and submit button. The process of searching is a ‘service’ the page provides, it will be provided as a method. To separate concerns, your test should have access to the services, but no knowledge of the underlying HTML. For this reason elements are private members and services are public methods. Services should return information about the page, or new Page Objects. Your search page may look something like this, notes below.

package com.PeterNewhook;

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

public class SearchPage extends Page{

	public SearchPage(WebDriver driver) {
		super(driver);
	}

	private WebElement q; //Search box
	private WebElement go; //Search button

	public ResultsPage search(String searchStatement){
		sb_form_q.sendKeys(searchStatement);
		sb_form_go.click();
		return PageFactory.initElements(_driver, ResultsPage.class);
	}
}

Notice a few things a few things about this class

  • It extends Page
  • search returns a ResultsPage object
  • q and go fields are used without using being instantiated.

Page is my own class with a single constructor that requires a WedDriver object. Inheriting this class makes it easy for the PageObject class to instantiate your object and associate a driver with it. Unfortunately, I missed this in the WebDriver documentation and had to check the PageFactory source after seeing this in a few examples. The Page class would look something like this:

package com.PeterNewhook;

import org.openqa.selenium.WebDriver;

public class Page {

	WebDriver _driver;
	public Page(WebDriver driver){
		this._driver=driver;
	}
}

By returning a ResultsPage object, the search method (or ‘service’ in Page Object parlance) allows our tests to navigate through the application without any reliance of the structure. This page would be instantiated with all the services a search results page would have. It might look something like this.

package com.PeterNewhook;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

public class ResultsPage extends Page{

	public ResultsPage(WebDriver driver) {
		super(driver);
	}

	private WebElement count;

	public String getPagesReturned(){
		return count.getText();
	}
}

Ordinarily you would expect using q and go this way to throw a Null Pointer exception. If you’re familiar with the WebDriver API you may expect to see something like driver.findElements(By.id(“q”);. But therein lies the magic of the PageObject class. Take a look at how the SearchPage could be used by a full WebDriver program.

package com.PeterNewhook;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.PageFactory;

public class SearchRunner {

	public static void main(String[] args) {
		WebDriver driver = new FirefoxDriver();
		driver.get("http://bing.com/");
		SearchPage bingHome = PageFactory.initElements(driver, SearchPage.class);
		ResultsPage searchResults = bingHome.search("Page Object Pattern");
		System.out.println(searchResults.getPagesReturned());
		driver.close();
	}
}

The PageFactory.initElements static method takes your driver instance and the class type you want returned, and returns a Page Object with it’s fields fully initialized. By default, the PageFactory will search for elements on the page with a matching id. If that fails, it will search by the name attribute. Because q is the name of the search box, the element is found automatically, however it could have also been defined using the FindBy attribute

@FindBy(how = How.NAME, using = "q")
private WebElement searchBox;

or even more simply

@FindBy(name "q")
private WebElement searchBox;

Other location strategies, like xpath or className, are also available using the FindBy attribute. I generally prefer using descriptive name for my field names, so I like to explicitly declare the FindBy method. This also gives me the flexibility to change the field name at a later date without needing to hunt down variables used throughout the class.

Now that you have a basic understanding of the Page Object pattern, I strongly suggest reading Simon Stewart’s wiki page mentioned earlier. It gives great detail and goes into depth on the nature of the PageFactory helper class.

9 thoughts on “Automated Testing Using Page Objects and WebDriver

  1. Michael

    This is one of the clearest explanations of PageFactory that I have read, I’ve been struggling with this for a while as a java and webdriver/selenium 2 newbie.  Thank you for writing it!

    I have one question: in your SearchPage class, you have the following locators:
    private WebElement q; //Search box
    private WebElement go; //Search buttonThis looks magical to me. How does webdriver know how to find these elements? It seems like these lines should be more like this: private WebElement q = driver.findElement(By.name(“q”));Thank you again for a great blog post!MikeD

  2. Igor Kravcov

    That is a brilliant article and helped me a lot putting the things together. I have however one question. I implemented the same pattern in c# .NET and had to change some things. I could not use q and go  without being instantiated. It would always return null so i had to do the following in my SearchPage class IWebElement query = _driver.FindElement(By.Name(“q”));
    Same goes for Results page, my count field was always null so i had to do something like this:

        public class ResultsPage : Page    {        public ResultsPage(IWebDriver driver)            : base(driver)        {        }        private IWebElement count;        public string GetPagesReturned()        {            count = _driver.FindElement(By.Id(“count”));            return count.Text;        }    }

    i ma just curious if this is the right thing to do? And why cant i use the field without them being instantiated in C# (just like you use in this Java sample). 

    1. Peter Newhook

      q and go need to be instantiated by the PageFactory class, which in .Net you would find in the OpenQA.Selenium.Support.PageObjects namespace. You need to pass the pageobject to the InitElements static method, like I did in line 19 of the first snippet.

      1. Igor Kravcov

        Hi Peter, thanks for your reply. I did exactly that but the problem i had with this approach was that i could not return PageFactory.InitElements as InitElements is a static void method. So what i did instead is this:

                public SearchPage(IWebDriver driver) : base(driver)        {        }        private IWebElement q;                private IWebElement go;        public ResultsPage Search(string searchStatement)        {            q.SendKeys(searchStatement);            go.Click();            ResultsPage page = new ResultsPage(_driver);            PageFactory.InitElements(_driver, page);            return page;        }    }

        however q and go would return null all the time so i have implemented it as follows:

                public SearchPage(IWebDriver driver) : base(driver)        {        }        private IWebElement _query;        public ResultsPage Search(string searchStatement)        {            _query = _driver.FindElement(By.Name(“q”));            _query.SendKeys(searchStatement);            _query = _driver.FindElement(By.Name(“go”));            _query.Click();            ResultsPage page = new ResultsPage(_driver);            PageFactory.InitElements(_driver, page);            return page;        }    }
        Which does work but i am not sure if i am taking the correct approach. I apologize i amy not now enough about the WebDriver as i just started looking at it, but any input would be much appreciated.

        1. Peter Newhook

          You know what? I’m actually not sure how the .Net PageObject pattern works. I’m going to play with it for a bit, then write a new post answering your question. I’ll post back here when I’m done.

  3. Anonymous

    Peter – that is an excellent (easy to understand) article.
    Looks like C# PageFactorydoes not  init private superclass elements (via PageFactory.InitElements) per this:
    http://code.google.com/p/selenium/issues/detail?id=1189#makechanges

    Also thanks to Igor Kravcov whose code I copied (your converted java to C# code).. here's all the code that I hope can be posted with your article since it will save a lot of C# developers a lot of time:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using OpenQA.Selenium;
    using OpenQA.Selenium.Firefox;
    using OpenQA.Selenium.IE;
    using OpenQA.Selenium.Support;
    //using OpenQA.Selenium.Support.PageObjects;

    // using OpenQA.Selenium.Support.PageObjects; // contains pagefactory but it does not work
    // based on this: http://www.peternewhook.com/2010/09/automated-testing-pageobjects-webdriver/#disqus_thread
    // most C# code copied from Igor Kravcov (thank you!)

    namespace ConsoleApplication1
    {
    public class SearchRunner
    {

    IWebDriver _driver;

    public void SearchRunnerX()
    {
    _driver = new FirefoxDriver();
    // _driver = new InternetExplorerDriver();
    _driver.Url = "http://bing.com/";
    _driver.Navigate();

    SearchPage bingHome = new SearchPage(_driver);
    // PageFactory.InitElements(_driver, bingHome);
    ResultsPage objResultsPage = bingHome.Search("Page Object Pattern");
    Console.WriteLine("searchResults: {0}", objResultsPage.GetPagesReturned());
    _driver.Close();
    }
    }
    }

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using OpenQA.Selenium;
    // constains PageFactory but it does not initialize page properties (broken)
    // using OpenQA.Selenium.Support.PageObjects;

    namespace ConsoleApplication1
    {
    public class SearchPage : Page
    {
    public SearchPage(IWebDriver driver)
    : base(driver)
    {
    }

    public IWebElement q; //Search box
    public IWebElement go; //Search button

    public ResultsPage Search(string searchStatement)
    {
    Type objDriverType = _driver.GetType();
    q = _driver.FindElement(By.Name("q"));
    go = _driver.FindElement(By.Name("go"));
    q.SendKeys(searchStatement);
    go.Click();
    if (objDriverType.Name == "InternetExplorerDriver")
    { // an extra click is needed to get out of the autofill-dropdown of the search box. this last click works
    go.Click();
    }
    // this code works but it is not needed so far and I'm not sure if it will accomplish what is needed - testing will need to be done
    //TimeSpan objTimeSpan = TimeSpan.Parse("00:00:33");
    // _driver.Manage().Timeouts().ImplicitlyWait(objTimeSpan);

    ResultsPage objResultsPage = new ResultsPage(_driver);

    // PageFactory.InitElements(_driver,objResultsPage);
    return objResultsPage;
    }
    }
    }

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using OpenQA.Selenium;

    namespace ConsoleApplication1
    {
    public class ResultsPage : Page
    {
    public ResultsPage(IWebDriver driver)
    : base(driver)
    {
    }

    private IWebElement count;

    public string GetPagesReturned()
    {
    count = _driver.FindElement(By.Id("count"));
    return count.Text;
    }
    }
    }

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using OpenQA.Selenium;

    namespace ConsoleApplication1
    {
    public class Page
    {
    public IWebDriver _driver;

    public Page(IWebDriver driver)
    {
    this._driver = driver;
    }
    }
    }

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    namespace ConsoleApplication1
    {
    class Program
    {
    static void Main(string[] args)
    {
    SearchRunner searchrunner = new SearchRunner();
    searchrunner.SearchRunnerX();
    }
    }
    }

  4. Rob

    To use OpenQA.Selenium.Support.PageObjects you must give the web elements attributes, the PageFactory will only initialize the object if they have valid attributes, I’ve included the updated SearchPage class below

    public class SearchPage : Page
    {
    public SearchPage(IWebDriver driver)
    : base(driver)
    {
    }

    [FindsBy(How = How.Name, Using = "q")]
    [CacheLookup]
    public IWebElement q; //Search box

    [FindsBy(How = How.Name, Using = "go")]
    [CacheLookup]
    public IWebElement go;
    public ResultsPage Search(string searchStatement)
    {
    q.SendKeys(searchStatement);
    go.Click();
    ResultsPage page = new ResultsPage(_driver);
    PageFactory.InitElements(_driver, page);
    return page;
    }
    }

Comments are closed.