Wednesday, September 14, 2011

The PageObject pattern for Selenium WebDriver UI tests

There was a bug recently encountered while browsing the graphs page. Clicking on two of the canned graph options yielded this friendly result:

Woops. So at that moment I was also experimenting with Selenium WebDriver for automating some UI tests. So I figured "hey, why not reproduce the problem with some of these tests before fixing it."


At first I just stuck the Selenium test code in each test case, but after awhile I refactored to use the page object pattern. Basically you have a class that represents some part of the page (in this case, GraphsPage) which serves as an interface the the component's services, such as "generate graph." in our case.



So before the first step, it's time to define a base class called PageObject which we'll subclass for the graphs page. At this point I'll also note that Selenium seems to have some kind of support integrated called a PageFactory that you use on your page object class. In the near future I might refactor them again to use that.

But anyway, here's the PageObject:
package pages;

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

public abstract class PageObject 
{
    protected WebDriver source; 
    
    protected PageObject(WebDriver source)
    {
        this.source = source; 
    }
                
    public abstract String getURL(); 
    
    public abstract void focus(); 
}

Nothing too exciting here, it's just some boiler plate.

On to the fun part, the GraphsPage class:

package pages.graphs;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;

import pages.PageObject;

public class GraphsPage extends PageObject
{
    private String GRAPH_URL = "graphs"; 
    
    public GraphsPage(WebDriver driver)
    {
        super(driver); 
    }
    
    private WebElement getGeneratedURLLabel()
    {
        return source.findElement(By.cssSelector("#graphpanel #graphlink")); 
    }
    
    private  Select getCannedGraphsList()
    {
        return new Select(source.findElement(By.cssSelector("#cannedgraphs select")));
    }
    
    private  WebElement getGenerateGraphsButton()
    {
        return source.findElement(By.className("getgraph"));
    }
    
    public String getURL()
    {
        return GRAPH_URL; 
    }
    
    public boolean generateGraph()
    {
        getGeneratedURLLabel().clear(); 
        getGenerateGraphsButton().click();  
        
        try
        {            
            (new WebDriverWait(source, 5)).until(new ExpectedCondition<Boolean>() {
                public Boolean apply(WebDriver d) {
                    return getGeneratedURLLabel().getText().contains("?"); 
                }
            });
            
            return true; 
        }
        catch(Exception exc)
        {
            return false; 
        }
    }
    
    public void selectCannedGraph(String cannedGraphTitle)
    {
        Select cannedGraphs = getCannedGraphsList();         
        cannedGraphs.selectByVisibleText(cannedGraphTitle); 
        cannedGraphs.getFirstSelectedOption().click(); 
    }

    @Override
    public void focus() 
    {
        getGeneratedURLLabel().click(); 
    }
}

Alright, now for the explanation. First, I'd like to explain the presence of the focus method. It seems in my experience that using FireFox 6 and IE, the click() method would sometimes fail to work unless you focused the page the first time. So that's what that does.

Here are the service methods:
  • generateGraph(): clicks the "generate graph" button, and returns true if the graph was retrieved successfully. This is done by checking for the presence of a generated URL for that specific graph. 
  • selectCannedGraph(): selects the canned graph on the canned graph list based on it's title.
You also see a few private methods, those are there mainly to abstract references to specific HTML elements and only have one reference to them.

Now we can write a test case to reproduce the problem. The problem occurs when you select the canned graph "Most played in cities," so let's write one to reproduce that:

package http.graphs;

import static org.junit.Assert.assertTrue;
import http.SeleniumTest;

import org.junit.Test;

import pages.graphs.GraphsPage;

public class TestGraphs extends SeleniumTest
{
    
    @Test
    public void testMostPlayedInCities()
    {
        testCannedGraph_helper("Most played-in cities");        
    }
    
    private void testCannedGraph_helper(String graphToCheck)
    {
        final GraphsPage page = new GraphsPage(driver);         
        driver.get(super.getPage(page.getURL())); 
        
        page.selectCannedGraph(graphToCheck); 
        assertTrue(page.generateGraph()); 
    }        
}


Nothing too exciting, you can see I've added an additional helper method so we can test other canned graphs as well. All we do here is select the canned graph, generate it, and assert that the graph was generated correctly. If you run it, it will fail.

One last thing, you can see that this test class extends SeleniumTest. That's just a boiler plate class, it looks like this:

package http;

import org.junit.After;
import org.junit.Before;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class SeleniumTest 
{
    protected WebDriver driver;
    protected final String ROOT = "http://localhost:8080/Recordings/"; 
    
    protected WebDriver getNewDriver()
    {
        return new FirefoxDriver(); 
    }
    
    @Before
    public void Before()
    {
        driver = getNewDriver(); 
    }
    
    @After
    public void After()
    {
        driver.quit(); 
    }
    
    public String getPage(String page)
    {
        return ROOT + page; 
    }
}

It just initializes the Firefox drvier and restarts it after each test. Alright, so next time I'll show you what the actual problem is and how it was fixed. Until then, take care!

No comments:

Post a Comment