Using the Page Object Design pattern in Selenium Webdriver

In a previous post, we have seen how using an object map significantly reduces the amount of maintenance needed on your Selenium scripts when your application under test is updated. Using this object map principle minimizes duplication of code on an object level. In this post, I will introduce an additional optimization pattern that minimizes code maintenance required on a higher level of abstraction.

Even though we have successfully stored object properties in a SPOM (a Single Point Of Maintenance), we still have to write code that handles these objects every time our script processes a given page including that object in our set of test scripts. If our set of test scripts requires processing a login form five times throughout the execution, we will need to include the code that handles the objects required to log in – a username field, a password field and a submit button, for example – five times as well. If the login page changes but the objects defined previously remain the same – for example, an extra checkbox is included to have a user agree to certain terms and conditions – we still need to update our scripts five times to include the processing of the checkbox.

To eliminate this code redundancy and maintenance burden, we are going to use a different approach known as the Page Object design pattern. This pattern uses page objects that represent a web page (or a form within a page, if applicable) to separate test code (validations and test flow logic, for example) from page specific code. It does so by making all actions that can be performed on a page available as methods of the page object representing that page.

So, assuming our test scripts needs to login twice (with different credentials), instead of this code:

public static void main(String args[]) {
	
	// start testing
	WebDriver driver = new HtmlUnitDriver();
		
	// first login
	driver.get("http://ourloginpage");
	driver.findElement(objMap.getLocator("loginUsername")).sendKeys("user1");
	driver.findElement(objMap.getLocator("loginPassword")).sendKeys("pass1");
	driver.findElement(objMap.getLocator("loginSubmitbutton")).click();
		
	// do stuff
		
	// second login
	driver.get("http://ourloginpage");
	driver.findElement(objMap.getLocator("loginUsername")).sendKeys("user2");
	driver.findElement(objMap.getLocator("loginPassword")).sendKeys("pass2");
	driver.findElement(objMap.getLocator("loginSubmitbutton")).click();
		
	// do more stuff
	
	// stop testing
	driver.close();
}

we would get

public static void main(String args[]) {
		
	// start testing
	WebDriver driver = new HtmlUnitDriver();
		
	// first login
	LoginPage lp = new LoginPage(driver);
	HomePage hp = lp.login("user1","pass1");
		
	// do stuff
		
	// second login
	LoginPage lp = new LoginPage(driver);
	HomePage hp = lp.login("user2","pass2");
		
	// do more stuff
		
	// stop testing
	driver.close();
}

Now, when we want to go to and handle our login page, we simply create a new instance of that page and call the login method to perform our login action. This method in turn returns a HomePage object, which is a representation of the page we get after a successful login action. A sample implementation of our LoginPage object could look as follows:

public class LoginPage {
	
	private final WebDriver driver;
	
	public LoginPage(WebDriver driver) {
		this.driver = driver;
		
		if(!driver.getTitle().equals("Login page")) {
			// we are not at the login page, go there
			driver.get("http://ourloginpage");
		}
	}
	
	public HomePage login(String username, String password) {
		driver.findElement(objMap.getLocator("loginUsername")).sendKeys("username");
		driver.findElement(objMap.getLocator("loginPassword")).sendKeys("password");
		driver.findElement(objMap.getLocator("loginSubmitbutton")).click();
		return new HomePage(driver);
	}	
}

It contains a constructor that opens the login page if it is not visible already. Alternatively, you could throw an exception and stop test execution whenever the login page is not the current page, depending on how you want your test to behave. Our LoginPage class also contains a login method that handles our login actions. If ever the login screen changes, we only need to update our test script once thanks to the proper use of page objects.

When the login action is completed successfully, our test returns a HomePage object. This class will be set up similar to the LoginPage class and provide methods specific to the page of our application under test it represents.

In case we also want to test an unsuccessful login, we simply add a method to our LoginPage class that executes the behaviour required:

public LoginPage incompleteLogin(String username) {
	driver.findElement(objMap.getLocator("loginUsername")).sendKeys("username");
	driver.findElement(objMap.getLocator("loginSubmitbutton")).click();
	return this;
}

This alternative login procedure does not enter a password. As a result, the user is not logged in and the login page remains visible, hence we return the current LoginPage object here instead of a HomePage object. If we want to test this type of incorrect login in our script, we simply call our new incorrectLogin method:

public static void main(String args[]) {
		
	// start testing
	WebDriver driver = new HtmlUnitDriver();
		
	// incorrect login
	LoginPage lp = new LoginPage(driver);
	lp = lp.incompleteLogin("user1");
	Assert.assertEquals("You forgot to type your password",lp.getError());
		
	//stop testing
	driver.quit();
}

The getError method is implemented in our LoginPage class as well:

public String getError() {
	return driver.findElement(objMap.getLocator("errorField")).getText();
}

This getError method is the result of another best practice. In order to keep your test code as much separated from your object code, always place your assertions outside of your page objects. If you need to validate specific values from a page, write methods that return them, as we did in the example above using the getError method.

To wrap things up, using the Page Object design pattern, we introduced another Single Point of Maintenance or SPOM in our Selenium test framework. This means even less maintenance required and higher ROI achieved!

An example Eclipse project using the pattern described above can be downloaded here.

Building and using an Object Repository in Selenium Webdriver

One of the main burdens of automated GUI test script maintainability is the amount of maintenance needed when object properties change within the application under test. A very common way of minimizing the time it takes to update your automated test scripts is the use of a central object repository (or object map as it’s also referred to sometimes). A basic object repository can be implemented as a collection of key-value pairs, with the key being a logical name identifying the object and the value containing unique objects properties used to identify the object on a screen.

Selenium Webdriver offers no object repository implementation by default. However, implementing and using a basic object repository is pretty straightforward. In this article, I will show you how to do it and how to lighten the burden of test script maintenance in this way.

Note that all code samples below are written in Java. However, the object repository concept as explained here can be used with your language of choice just as easily.

Creating the object repository
First, we are going to create a basic object repository and fill it with some objects that we will use in our test script. In this article, I am going to model a very basic scenario: go to the Bing search engine, search for a particular search query and determine the number of search results returned by Bing. To execute this scenario, our script needs to manipulate three screen objects:

  • The textbox where the search string is typed
  • The search button to be clicked in order to submit the search query
  • The text field that displays the number of search results

Our object map will a simple .properties text file that we add to our Selenium project:
Our object mapThe key for each object, for example bing.homepage.textbox, is a logical name for the object that we will use in our script. The corresponding value consists of two parts: the attribute type used for uniquely identifying the object on screen and the corresponding attribute value. For example, the aforementioned text box is uniquely identified by its id attribute, which has the value sb_form_q.

Retrieving objects from the object repository
To retrieve objects from our newly created object map, we will define an ObjectMap with a constructor taking a single argument, which is the path to the .properties file:

public class ObjectMap {
	
	Properties prop;
	
	public ObjectMap (String strPath) {
		
		prop = new Properties();
		
		try {
			FileInputStream fis = new FileInputStream(strPath);
			prop.load(fis);
			fis.close();
		}catch (IOException e) {
			System.out.println(e.getMessage());
		}
	}

The class contains a single method getLocator, which returns a By object that is used by the Selenium browser driver object (such as a HtmlUnitDriver or a FirefoxDriver):

public By getLocator(String strElement) throws Exception {
		
		// retrieve the specified object from the object list
		String locator = prop.getProperty(strElement);
		
		// extract the locator type and value from the object
		String locatorType = locator.split(":")[0];
		String locatorValue = locator.split(":")[1];
		
		// for testing and debugging purposes
		System.out.println("Retrieving object of type '" + locatorType + "' and value '" + locatorValue + "' from the object map");
		
		// return a instance of the By class based on the type of the locator
		// this By can be used by the browser object in the actual test
		if(locatorType.toLowerCase().equals("id"))
			return By.id(locatorValue);
		else if(locatorType.toLowerCase().equals("name"))
			return By.name(locatorValue);
		else if((locatorType.toLowerCase().equals("classname")) || (locatorType.toLowerCase().equals("class")))
			return By.className(locatorValue);
		else if((locatorType.toLowerCase().equals("tagname")) || (locatorType.toLowerCase().equals("tag")))
			return By.className(locatorValue);
		else if((locatorType.toLowerCase().equals("linktext")) || (locatorType.toLowerCase().equals("link")))
			return By.linkText(locatorValue);
		else if(locatorType.toLowerCase().equals("partiallinktext"))
			return By.partialLinkText(locatorValue);
		else if((locatorType.toLowerCase().equals("cssselector")) || (locatorType.toLowerCase().equals("css")))
			return By.cssSelector(locatorValue);
		else if(locatorType.toLowerCase().equals("xpath"))
			return By.xpath(locatorValue);
		else
			throw new Exception("Unknown locator type '" + locatorType + "'");
	}

As you can see, objects can be identified using a number of different properties, including object IDs, CSS selectors and XPath expressions.

Using objects in your test script
Now that we can retrieve objects from our object map, we can use these in our scripts to execute the desired scenario:

public static void main (String args[]) {

		// Create a new instance of the object map
		ObjectMap objMap = new ObjectMap("objectmap.properties");

		// Start a browser driver and navigate to Google
		WebDriver driver = new HtmlUnitDriver();
        driver.get("http://www.bing.com");

        // Execute our test
        try {
        	
        	// Retrieve search text box from object map and type search query
        	WebElement element = driver.findElement(objMap.getLocator("bing.homepage.textbox"));
			element.sendKeys("Alfa Romeo");
			
			// Retrieve search button from object map and click it
			element = driver.findElement(objMap.getLocator("bing.homepage.searchbutton"));
			element.click();
			
			// Retrieve number of search results using results object from object map
			element = driver.findElement(objMap.getLocator("bing.resultspage.results"));
			System.out.println("Search result string: " + element.getText());
			
			// Verify page title
			Assert.assertEquals(driver.getTitle(), "Alfa Romeo - Bing");
			
		} catch (Exception e) {
			System.out.println("Error during test execution:\n" + e.toString());
		}
        
	}

You can see from this code sample that using an object from the object map in your test is as easy as referring to its logical name (i.e., the key in our object map).

Object repository maintenance
With this straightforward mechanism we have been able to vastly reduce the amount of time needed for script maintenance in case object properties change. All it takes is an update of the appropriate entries in the object map and we’re good to go and run our tests again.

Thanks to Selenium Master for explaining this concept clearly for me to apply.

An example Eclipse project using the pattern described above can be downloaded here.

Running Selenium Webdriver tests in Jenkins using Ant

In a previous post I introduced a very simple and straightforward way to run data-driven tests in Selenium Webdriver using test data stored in an Excel sheet. In this post, I want to show how to run these tests using a continuous integration (CI-) solution.

My preferred CI-tool is Jenkins, as it is open source, very flexible and easy to use.
First, make sure that Jenkins is set up properly and is running as a service. Installation is very easy, so I won’t go into details here.

I also recommend using Ant as a software build tool to further ease the process of compiling and running our tests. While it is not strictly necessary to use Ant, it will make life a lot easier for us. Again, install Ant and make sure it is running smoothly by typing ant on the command prompt. If it starts asking for a build.xml file, it’s running properly.

ant_running

Next, open the Selenium Webdriver project in your IDE. Again, I prefer using Eclipse, so the images shown here will be based on Eclipse. Other IDEs such as IntelliJ usually provide the same functionality, it’s just hidden behind different menu options.

Before we start configuring our project for using Ant and running in Jenkins, we are going to add some flexibility to it. Jenkins uses its own workspace and might be running on another server altogether. However, our Selenium project contains a reference to a locally stored Excel data source. Therefore, we are going to add the possibility to provide the path to the data source to be used as an argument to our main method in the Selenium test. In this way, we can simply specify the location of the data source when we run the test. Not only is this necessary to have our test run smoothly in Jenkins, it also adds the possibility to execute several test runs with separate test data sets.

public static void main (String args[]) {
		
		String strPath;
		
		if (args.length == 1) {
			strPath = args[0];
		} else {
			strPath = "Z:\\Documents\\Bas\\blog\\datasources\\testdata.xls";
		}
		
		try {
			// Open the Excel file
			FileInputStream fis = new FileInputStream(new File(strPath).getAbsolutePath());

If an argument is specified when running the main method of our test, we assume this is a relative path to our Excel data source. In order to be able to use it, all we have to do is to get the absolute path for it and off we go. If no argument is specified, we use the default Excel file.

Next, we create a build.xml file that provides Ant with the necessary instructions and details on how to build and run our test. In Eclipse, this can be done easily by right-clicking our project and selecting ‘Export > General > Ant Buildfiles’. After selecting the appropriate project, a build.xml file is generated and added to the root of our project. The example below is a part of the resulting file:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- WARNING: Eclipse auto-generated file.
              Any modifications will be overwritten.
              To include a user specific buildfile here, simply create one in the same
              directory with the processing instruction <?eclipse.ant.import?>
              as the first entry and export the buildfile again. --><project basedir="." default="build" name="seleniumTest">
<property environment="env"/>
<property name="ECLIPSE_HOME" value="C:/Tools/eclipse"/>
<property name="debuglevel" value="source,lines,vars"/>
<property name="target" value="1.7"/>
<property name="source" value="1.7"/>
<path id="seleniumTest.classpath">
    <pathelement location="bin"/>
    <pathelement location="../../../../../vmware-host/Shared Folders/Documents/Bas/blog/libs/selenium-server-standalone-2.37.0.jar"/>
    <pathelement location="../../../../../vmware-host/Shared Folders/Documents/Bas/blog/libs/poi-3.9-20121203.jar"/>
</path>
<target name="init">
    <mkdir dir="bin"/>
    <copy includeemptydirs="false" todir="bin">
        <fileset dir="src">
            <exclude name="**/*.java"/>
        </fileset>
    </copy>
</target>
<target name="clean">
    <delete dir="bin"/>
</target>

Before we can run our tests using Ant, we need to make two modifications.

First, we need to clean up the classpath as we want to use clear and relative paths. Make sure that the necessary libraries can be found on the designated locations. These are specified relative to the location of the build.xml file.

<path id="seleniumTest.classpath">
    <pathelement location="bin"/>
    <pathelement location="libs/selenium-server-standalone-2.37.0.jar"/>
    <pathelement location="libs/poi-3.9-20121203.jar"/>
</path>

Next, we need to make sure that our Excel data source is specified as an argument in the designated Ant target:

<target name="ExcelDataDriven">
    <java classname="com.ontestautomation.selenium.ExcelDataDriven" failonerror="true" fork="yes">
        <arg line="datasources/testdata.xls"/>
        <classpath refid="seleniumTest.classpath"/>
    </java>
</target>

Now, we can execute our test using Ant either in Eclipse or at the command line. When you choose the latter, go to the subdirectory for the Selenium project in your workspace (build.xml should be located there) and execute ant <> (ant ExcelDataDriven in this case). You’ll see that the test is run successfully using Ant.

ant_run_test

The final step is to have this step performed by Jenkins. This should be very straightforward now. Create a new job in Jenkins and add ‘Invoke Ant’ as a build step. Specify the correct target (again, ExcelDataDriven in our case).

jenkins_ant_target

Make sure that all referenced libraries and data sources can be found at the correct locations in the workspace for the Jenkins job (this is where using relative paths comes in handy!). Normally, you would do this using some sort of version control sytem such as Subversion. Next, schedule a build for the job, which should run smoothly now:

jenkins_test_output

That’s it, we’ve now successfully run our Selenium Webdriver tests in Jenkins using Ant. One step closer to a successful continuous integration approach!