Tuesday, June 19, 2012

Creating a Generic Advanced Page Factory

One of the problems in web testing you may encounter is having to support different pagesets and alternate flows.  A common solution to that is to use interfaces and factories to provide an abstraction between the high level tests and low level implementation logic.  But this can get cumbersome writing a whole host of custom factories to support all the different pagesets or alternate pages.  Do you put all these into separate factories or one huge one?  Is all this factory code a time sink to maintain?

When I started my new job at carsdirect.com, one of the biggest challenge we faced was that the company did a lot of A/B testing, and almost all our pages have different pagesets that looked completely different, and many of them were designed before automation was even a consideration.  We can have typically 4 versions of the same  page, and it's quite common that few of the pagesets did not share any of the same ids or css selectors.

To solve this problem, I wanted a way to provide an interface for those pages, then subclass them to manage various variations.  However, hard coding factories to switch between servering the different variations were becoming cumbersome.  So ideally we wanted to have one factory that can do it all for us.

Introducing Generics & Reflection

Introduced in java 5, the ability of using generics, combined with Java's reflections api (http://code.google.com/p/reflections/), we are able to query for classes implementing a interface we pass in.  Using that set of classes, we can attempt to instantiate them in a loop then return the best match.  Here we use reflections to query for Page Objects matching that interface.  Generics is used to infer the type returned using the interface passed in, this avoids unnecessary type casting as Java will know it returns the exact same type as the interfaced passed in.

public static <T> T constructPage(WebDriver driver,
            Class<T> interfaceToProxy) {

        // Get interface namespace.
        String namespace = interfaceToProxy.getPackage().getName();

        // Get a set of classes that implements the interfaces.
        Reflections reflections = new Reflections(namespace);
        Set<class<? extends T>>; subTypes = reflections

        for (Class classObj : subTypes) {
            try {
                T pageInstance = PageFactory.initElements(driver, classObj);

                // Page object instantiated correctly, we have found a match to
                // return.
                System.out.println("Found a matching PageObject: "
                        + classObj.getName());
                return pageInstance;
            } catch (Exception e) {
                //These are not the droids you're looking for.


        return null;

Each page object will be able to check if it's a matching page object in the constructor.  Here each page object can do a check to see if it's the right page object for the job.

public class GoogleSearchPage implements ISearchPage {
    public GoogleSearchPage(WebDriver driver) {
        if (!driver.getCurrentUrl().contains("google.com")) {
            throw new IllegalStateException("This is not Google Page");
    public void search(String query) {

  Here's an example of consuming this.

public void test() {
        ISearchPage page = AdvancedPageFactory.constructPage(driver,
        //See we are on a Google page.
        assertTrue(page instanceof GoogleSearchPage);
        assertFalse(page instanceof YahooPageSearchPage);
        page = AdvancedPageFactory.constructPage(driver,
        //See we are on a Yahoo page.
        assertTrue(page instanceof YahooPageSearchPage);
        assertFalse(page instanceof GoogleSearchPage);

Here we just simple pass it the ISearchPage interface, and our Advanced Page Factory will find the matching page and return it to us.  None of the messy if/else or switch statements that we normally would have to do in a hard coded factory.

Download the full working example: project_files

To run this example, you will need Java 1.6 or greater installed, and Maven 2/3.

No comments: