Selenium

Test Design Patterns

Created by Kavan Sheth



Best viewed in Chrome, Firefox, Opera or A browser supporting HTML5. Use arrow keys ( up ↑, down ↓, left ←, right ↑) to navigate. Uses Reveal.js by Hakim El Hattab Press ESC to enter the slide overview.

Introduction

  • There are few good design patterns exist which you should follow while developing your frameworks using selenium.
  • Here we will discuss few based on my knowledge, you can use whichever suites to your needs.
  • Don't use a pattern for sack of using, only go for it if you find it worth in terms of code maintainability, flexibility, re-usability.
  • But if you are using correct pattern, such patterns can be very helpful in long run.

UI Map or Object Repository



Why?

  • Consider a large project with plenty of xpaths/CSSselectors used, how will you manage them. Whenever an xpaths/CSSselectors changes you have to search through your code and change xpath, build and run tests again.
  • By looking at xpaths/CSSselectors it is hard to tell what it refers, and as project pass through multiple hands it becomes harder to track down xpaths and impact of UI change on them.
  • UI mapping is a way to isolate xpaths/CSSselectors from your actual code.
  • In Selenium 1(Selenium RC), locators were not distinguished using By class, so all locators provided with type or click command as String.
  • But in Selenium 2 because of BY class, you need to specify locator as well in your UI map.

Example From Selenium Docs

(with Selenium 1)

Create a class for UI mapping

import java.io.FileInputStream;
import java.util.Properties;					
public class AdminUiMap {
    public String username;

    public AdminUiMap(String fileName) {
        Properties props = new Properties();
        props.load(new FileInputStream(fileName));

        this.username = props.getProperty("admin.username");
    }
}

And then create admin object of AdminUiMap Class

AdminUiMap admin = new AdminUiMap("adminLocators.properties");

Now you can use strings from your property file as

selenium.type(admin.username, "xxxxxxxx");
source: http://stackoverflow.com/questions/11588083/using-a-properties-file-as-a-ui-mapping-in-java
  • If you do not want to keep mapping in a file, you can also just keep it in a class
  • public class AllUIMaps { 
    public static class LoginPage 
    { 
    	private static String username ="user_Name";
    
    	public String getUsername() {
    		return username;
    	}
    
    	public void setUsername(String username) {
    		LoginPage.username = username;
    	}	
    } 
    public static class userDetails{ 
    	private static String userphone="user_phone"; 
    	private static String useraddr="user_addr1";
    	
    	public String getUserphone() {
    			return userphone;
    	}
    	public void setUserphone(String userphone) {
    		userDetails.userphone = userphone;
    	}
    	public String getUseraddr() {
    		return useraddr;
    	}
    	public void setUseraddr(String useraddr) {
    		userDetails.useraddr = useraddr;
    	}
    } 
    } 
    					
  • So you can just use required mapping using getter method
  • Here subclass is used to separate data of different context. you can use it to separate mappings of different pages.

Page Object Model


Example


Here we will directly start with one example and I think it will help you to understand how page object model developed and how can you use it for your project.

We will use http://newtours.demoaut.com/, it is dummy site for QTP.

Scenario:find flights for a target

Steps:

  • open http://newtours.demoaut.com/
  • Login using username 'test' and password 'test'
  • find list of flights for a One way NewYork to London Business class journey for 25th October on unified airlines.

Try It!!

One might write code like following:

import java.io.File;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;

public class WithoutPageObject{
	@Test
	public void testFindFlightForMe()
	{		
				WebDriver driver;
				File file = new File("C:\\Users\\xxxxx\\Desktop\\chromedriver.exe");        
			    System.setProperty("webdriver.chrome.driver", file.getAbsolutePath());
			    driver = new ChromeDriver();
			    String baseUrl = "http://newtours.demoaut.com/";
			    driver.manage().timeouts().implicitlyWait(180, TimeUnit.SECONDS);
			    driver.get(baseUrl);
			    driver.findElement(By.name("userName")).sendKeys("test");
			    driver.findElement(By.name("password")).sendKeys("test");
			    
			    driver.findElement(By.name("login")).click();
			    			    
			    driver.findElement(By.xpath("//input[@name='tripType'][@value='oneway']")).sendKeys(Keys.SPACE);
			    new Select(driver.findElement(By.name("fromPort"))).selectByValue("New York");
			    new Select(driver.findElement(By.name("toPort"))).selectByValue("London");
			    new Select(driver.findElement(By.name("fromDay"))).selectByValue("25");
			    driver.findElement(By.xpath("//input[@name='servClass'][@value='Business']")).sendKeys(Keys.SPACE);
			    new Select(driver.findElement(By.name("airline"))).selectByVisibleText("Unified Airlines");
			    
			    driver.findElement(By.name("findFlights")).click();
			    
			    //here you can have some verification of retrieved result
			    driver.close();
	}
}

From Coding point of view nothing wrong with above code, but there are few points one must consider

  • Login and flight search activities may be part of multiple test cases and repeating it will be just line multiplier and will not add any value to your tests.
  • Also this code looks messy, if you read it after few days, it will take some time to understand what this test is doing.
  • if something gets changed on a page, you will have to review all testcases to estimate its impact. so here tests are tightly coupled with its implementation.

Page Object Model

  • Page Object Model provides Object Oriented Solution of issues considered above.
  • Here we will create PageObjects representing each page or sub page.
  • Characteristics of PageObjects
    • PageObjects should represent services offered by a page. like entering value to an input, clicking an element, get value of Element.
    • PageObjects should be only thing which have deep knowledge of HTML. Tests should only use services provided by Page Objects without knowledge of implementation.
    • PageObjects seldom expose webDriver. To achieve this PageObjects services(methods) should return other pageObjects. means if after using a method you are still staying on same page, method should return same pageObject, which can be used to call another service on that page. and if you are redirected to a new page, service should return different pageObject which represents services of this new page. This might sound odd or difficult at first place, but once you will have grip you will enjoy the way your code flows through pages
    • Very nice introduction given at https://code.google.com/p/selenium/wiki/PageObjects>

Creating Page Objects

Here we are dealing with three pages

  • LoginPage(main page)
  • FlightFinderPage
  • FlightFinderResultPage


Here in LoginPage Source. you can see that:

  • Class is having methods specifying services it is offering.
  • Each method returns same pageObject, Except method which causes redirection to other page
  • Each page object is having driver as member which initialized during class instantiation.

PageObjects

class LoginPage{
	protected WebDriver driver;
	
	public LoginPage(WebDriver d)
	{
		driver = d;
	}
	public LoginPage setUserName(String user)
	{
		driver.findElement(By.name("userName")).sendKeys(user);
		return this;
	}
	public LoginPage setPassword(String pass)
	{
		driver.findElement(By.name("password")).sendKeys(pass);
		return this;
	}
	public FlightFinderPage clickLoginButton()
	{
		driver.findElement(By.name("login")).click();
		return new FlightFinderPage(driver);
	}
}
class FlightFinderPage{

	protected WebDriver driver;
	public FlightFinderPage(WebDriver d) {
		driver = d;
	}
	public FlightFinderPage selectOneWayTrip()
	{
		driver.findElement(By.xpath("//input[@name='tripType'][@value='oneway']")).sendKeys(Keys.SPACE);
		return this;
	}
	public FlightFinderPage setFromPort(String fromPort)
	{
		new Select(driver.findElement(By.name("fromPort"))).selectByValue(fromPort);
		return this;
	}
	public FlightFinderPage setToPort(String toPort)
	{
		new Select(driver.findElement(By.name("toPort"))).selectByValue(toPort);
		return this;
	}
	public FlightFinderPage selectFromDay(String fromDay)
	{
		new Select(driver.findElement(By.name("fromDay"))).selectByValue(fromDay);
		return this;
	}
	public FlightFinderPage setBusinessClass()
	{
		driver.findElement(By.xpath("//input[@name='servClass'][@value='Business']")).sendKeys(Keys.SPACE);
		return this;
	}
	public FlightFinderPage selectAirline(String air)
	{
		 new Select(driver.findElement(By.name("airline"))).selectByVisibleText(air);
		 return this;
	}
	public FlightFinderResultPage clickFindFlights()
	{
		driver.findElement(By.name("findFlights")).click();
		return new FlightFinderResultPage(driver);
	}
}
class FlightFinderResultPage{

	protected WebDriver driver;
	public FlightFinderResultPage(WebDriver d) {
		driver = d;
	}
	//Services to be defined 
}

Test using PageObjects

After creating page object you can writing your test as following, which seems more elegant.

public class WithPageObject{
	String baseUrl;
	
	@Before
	public void beforeTest()
	{
		File file = new File("C:\\Users\\kavan.sheth\\Desktop\\chromedriver.exe");        
	    System.setProperty("webdriver.chrome.driver", file.getAbsolutePath());
	    baseUrl = "http://newtours.demoaut.com/";
	}
	
	@Test
	public void test1()
	{		
				WebDriver driver = new ChromeDriver();	    
			    driver.manage().timeouts().implicitlyWait(180, TimeUnit.SECONDS);
			    driver.get(baseUrl);
			 
			    LoginPage loginPage = new LoginPage(driver);
			    loginPage.setUserName("test")
			             .setPassword("test");
			    FlightFinderPage flightFinderPage=loginPage.clickLoginButton();
			    
			    flightFinderPage.selectOneWayTrip()
			    	            .setFromPort("New York")
			    				.setToPort("London")
			    				.selectFromDay("25")
			    				.setBusinessClass()
			    				.selectAirline("Unified Airlines");
			    			    
			    FlightFinderResultPage flightFinderResultPage = flightFinderPage.clickFindFlights();			    
			    
			    //here you can have some verification of retrieved result
			    driver.close();
	}
}	

Using

PageFactory

for Page Object Model



Why PageFactory?

  • Page Factory is nothing but enhancement over Page Object Model.
  • In earlier example of Page Objects, each method was finding elements separately, and we were writing code for finding elements explicitly in each method.
  • Now PageFactory provides a way to avoid writing repetitive findElements code.
  • Very good Reference for PageFactory is available at https://code.google.com/p/selenium/wiki/PageFactory
  • In Page Factory, you need to specify page elements as class members.
  • 	private WebElement userName;
    	private WebElement password;
    	private WebElement login;
  • And when you initialize page using PageFactory.initElements() method like following,
  • LoginPage loginPage=PageFactory.initElements(driver, LoginPage.class);
  • Page factory will create proxies for each element and no actual element finding will happen.(that's why you will not get NoSuchElementException if element is not present)
  • After initialization, every time you refer WebElement, it will search for that element on page
  • Page factory will search using name of WebElement members. First it will try to search by matching it with 'id' attribute and if it fails, then it will search it again using 'name' attribute
  • Once Page is initialized, you can use webElement directly in methods without any need of findElement method
  • 	public LoginPage setUserName(String user)
    	{
    			userName.sendKeys(user);
    			return this;
    	}

Few Points

  • For Initialization using initElements(WebDriver driver, java.lang.Class<T> pageClassToProxy), you need to define page Objects as public classes. so you need to put them in separate files. otherwise you will get following exception:
  • java.lang.RuntimeException: java.lang.IllegalAccessException: 
    Class org.openqa.selenium.support.PageFactory can not access a member 
    of class *.LoginPage with modifiers "public"
  • As discussed, PageFactory first try to find out an element using 'id' attribute and then with 'name' attribute. And As here we have only name attribute to identify WebElement, in first attempt pageFactory will fail and it will wait till implicitWait time configured. which will cause delay in execution.

Source Code

Though here I have shown, all classes at same place, don't forget to separate page object classes in different files.

package net.mylearning.pagefactory;

import java.io.File;
import java.util.concurrent.TimeUnit;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;


public class WithPageFactory{
	String baseUrl;
	WebDriver driver;
	
	@Before
	public void beforeTest()
	{
		File file = new File("C:\\Users\\xxxxxx\\Desktop\\chromedriver.exe");        
	    System.setProperty("webdriver.chrome.driver", file.getAbsolutePath());
	    baseUrl = "http://newtours.demoaut.com/";
	    driver = new ChromeDriver();	    
	    driver.manage().timeouts().implicitlyWait(2, TimeUnit.SECONDS);
	}
	
	@Test
	public void test1()
	{					
			    driver.get(baseUrl);
		
			    LoginPage loginPage= PageFactory.initElements(driver, LoginPage.class);
				//you can also place this in constructor like PageFactory.initElements(driver, this), but any ways we will need to create pageObject so kept it here.
			    FlightFinderPage flightFinderPage=loginPage.setUserName("test")
			             .setPassword("test")
			             .clickLoginButton();
			    
			    FlightFinderResultPage flightFinderResultPage=flightFinderPage.selectOneWayTrip()
			    	            .setFromPort("New York")
			    				.setToPort("London")
			    				.selectFromDay("25")
			    				.setBusinessClass()
			    				.selectAirline("Unified Airlines")
			    			    .clickFindFlights();
			     		    
			    //here you can have some verification of retrieved result
	}
	@After
	public void afterTest()
	{
		driver.close();
	}
}	
public class LoginPage{

	private WebElement userName;
	private WebElement password;
	private WebElement login;
	public WebDriver driver;
	
	public LoginPage(WebDriver driver )
	{
		this.driver= driver;
	}

	public LoginPage setUserName(String user)
	{
		userName.sendKeys(user);
		return this;
	}
	public LoginPage setPassword(String pass)
	{
		password.sendKeys(pass);
		return this;
	}
	public FlightFinderPage clickLoginButton()
	{
		login.click();
		return PageFactory.initElements(driver, FlightFinderPage.class);
	}
}
public class FlightFinderPage{

	public WebDriver driver;
	
	private WebElement fromPort;
	private WebElement toPort;
	private WebElement fromDay;
	private WebElement airline;
	private WebElement findFlights;
	
	
	public FlightFinderPage(WebDriver d) {
		driver = d;
	}
	public FlightFinderPage selectOneWayTrip()
	{
		driver.findElement(By.xpath("//input[@name='tripType'][@value='oneway']")).sendKeys(Keys.SPACE);
		return this;
	}
	public FlightFinderPage setFromPort(String from)
	{
		new Select(fromPort).selectByValue(from);
		return this;
	}
	public FlightFinderPage setToPort(String to)
	{
		new Select(toPort).selectByValue(to);
		return this;
	}
	public FlightFinderPage selectFromDay(String fDay)
	{
		new Select(fromDay).selectByValue(fDay);
		return this;
	}
	public FlightFinderPage setBusinessClass()
	{
		driver.findElement(By.xpath("//input[@name='servClass'][@value='Business']")).sendKeys(Keys.SPACE);
		return this;
	}
	public FlightFinderPage selectAirline(String air)
	{
		 new Select(airline).selectByVisibleText(air);
		 return this;
	}
	public FlightFinderResultPage clickFindFlights()
	{
		findFlights.click();
		return  PageFactory.initElements(driver, FlightFinderResultPage.class);
	}
}
public class FlightFinderResultPage{

	//Services to be defined 
}
  • You might have noticed that, we are still using driver to find element for which we do not have unique name or id attribute.
  • And also delay due to failure in identifying element in first try also causing delay. as you might have noticed that because of this I reduced implicitWait time to just 5 seconds. You may need to set it as per your project needs, so it is bad to have delay in many WebElement search.
  • Don't worry ... there is a solution for these issues.

Using Annotations

  • Using Annotation @FindBy with an webElement you can tell PageFactory that how to find web element.
  • @FindBy have two parameter how and using, we can use it as following:
  • @FindBy(how = How.NAME, using = "fromPort")
    private WebElement fPort;
  • So now you are free from using name attribute as field name for a webElement.
  • And you can tell PageFactory how to find element.
  • 'How' supports all formats supported by 'BY' class

Using Annotations - Continue

  • @FindBy for xpath search
  • @FindBy(how = How.XPATH, using = "//input[@name='tripType'][@value='oneway']")
    private WebElement oneWayTrip;
  • What if you want to initialize list of webElements? For this you need to use annotation @FindAll, it takes @FindBy as parameter and all elements found using it are placed in list.
  • @FindAll({@FindBy(xpath = “yourfirstxpath”)
    ,@FindBy(xpath = “yoursecondxpath”),@FindBy(xpath = “yourThirddxpath”)})
    public List resultElements;
  • One More thing, Here page initialization doesn't mean webElements are found only once during initialization, Actually whenever we call a method on the webElement it will search for that element on the page. So if you are sure that element will remain present on the page whenever you are referring it then you can restrict every time lookup using @CacheLookup annotation.
  • @FindBy(how = How.NAME, using = "fromPort")
    @CacheLookup
    private WebElement fPort;

Page Object classes with Annotations


import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class LoginPage{

	@FindBy(name="userName")
	private WebElement userName;
	
	@FindBy(name="password")
	private WebElement password;
	
	@FindBy(name="login")
	private WebElement login;
	
	public WebDriver driver;
	
	public LoginPage(WebDriver driver )
	{
		this.driver= driver;
	}

	public LoginPage setUserName(String user)
	{
		userName.sendKeys(user);
		return this;
	}
	public LoginPage setPassword(String pass)
	{
		password.sendKeys(pass);
		return this;
	}
	public FlightFinderPage clickLoginButton()
	{
		login.click();
		return PageFactory.initElements(driver, FlightFinderPage.class);
       
	}
}

public class FlightFinderPage{

	public WebDriver driver;
	
	@FindBy(how = How.NAME, using = "fromPort")
	private WebElement fPort;
	
	@FindBy(how = How.XPATH, using = "//input[@name='tripType'][@value='oneway']")
	private WebElement oneWayTrip;
	
	@FindBy(name="toPort")
	private WebElement toPort;
	
	@FindBy(name="fromDay")
	private WebElement fromDay;
	
	@FindBy(name="airline")
	private WebElement airline;
	
	@FindBy(name="findFlights")
	private WebElement findFlights;
	
	@FindBy(how = How.XPATH, using = "//input[@name='servClass'][@value='Business']")
	private WebElement businessClass;
	
	
	public FlightFinderPage(WebDriver d) {
		driver = d;
	}
	
	public FlightFinderPage selectOneWayTrip()
	{
		oneWayTrip.sendKeys(Keys.SPACE);
		return this;
	}
	public FlightFinderPage setFromPort(String from)
	{
		new Select(fPort).selectByValue(from);
		return this;
	}
	public FlightFinderPage setToPort(String to)
	{
		new Select(toPort).selectByValue(to);
		return this;
	}
	public FlightFinderPage selectFromDay(String fDay)
	{
		new Select(fromDay).selectByValue(fDay);
		return this;
	}
	public FlightFinderPage setBusinessClass()
	{
		businessClass.sendKeys(Keys.SPACE);
		return this;
	}
	public FlightFinderPage selectAirline(String air)
	{
		 new Select(airline).selectByVisibleText(air);
		 return this;
	}
	public FlightFinderResultPage clickFindFlights()
	{
		findFlights.click();
		return  PageFactory.initElements(driver, FlightFinderResultPage.class);
	}
}

public class FlightFinderResultPage{

	//Services to be defined 
}