Design Patterns In Selenium Automation part 1 pom

Design Patterns in Selenium WebDriver – Part I – Page Object Pattern

Automating manual tests is not only in vogue now-a-days but is a requirement for all projects. And, one of the automation tools currently in demand is Selenium WebDriver. The various language bindings for Selenium WebDriver – Java, Python, Ruby, Javascript etc – allow a developer to easily create an automation solution. Having said that, the main requirements for any automation solution should be:
* maintainability,
* reusability,
* reliability,
* modularity and,
* save development time.

And, this is where design patterns help. Gang of Four define design patterns as follows- "design patterns describe simple and elegant solutions to specific problems in object-oriented software design. Design patterns capture solutions that have developed and evolved overtime …". Thus, design patterns allows us to write reusable, reliable and modular code.

One of the often used patterns/model in Selenium world is Page Object Model allowing us to improve maintainability of automated tests. This will be the first amongst the many pattern we'll be concentrating on in our journey to learn how to utilize the various design patterns in our test framework.

In Page Object Model:
* Create classes that model/represent the various pages of the AUT (Application Under Test),
* Create methods that model the various interactions/behaviors we can perform on the page,
* Create tests to validate the AUT behaviors, states & data.

As a pre-requisite for this tutorial you would need the following installed:
* GradleBeginner's Gradle tutorial for Java projects ,
* Java SDK &,
* Eclipse/Intellij already installed along with TestNG plugins for the poison of your choice.

(You'll have to add gradle and Java bin folders to the system path. Also, for gradle you do not need to install groovy as it comes with it's own groovy installation.)

  1. As we'll be using Gradle for building our project & running the tests, we need to create a build file. Create one by navigating to the folder where the project will be created and running the following command
    cmd
    gradle init --type java-library

    This will not only create a build.gradle file for you, but also set up a project structure. The project structure will look something like this:

Project Structure

(I've added gradle & TestNG dependencies to the eclipse project. Also, add src/main/java & src/main/test folders as source folders in eclipse. You can do this by going to the context menu of the respective folder, going to Build & Add as source folder)

  1. Open the generated build.gradle file & copy-paste the following code (minus the multi-line comments at the top of the build file):

src/main/resources/build.gradle
“`groovy
// Apply the java plugin to add support for Java and eclipse
apply plugin: 'java'
apply plugin: 'eclipse'

// where to get the various dependency jars from
repositories {
mavenCentral()
}

// dependencies for the current project
dependencies {
compile 'org.seleniumhq.selenium:selenium-java:2.45.0'
//compile 'io.appium:java-client:2.2.0'
compile 'com.google.inject:guice:3.0'
// testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add
// 'test.useTestNG()' to your build script.
compile 'org.testng:testng:6.8.1'
}

// task Test defined here to run with TestNG
tasks.withType(Test) {
useTestNG{
// use this if you want to create TestNG report along with the build report generated
// by gradle
// else replace the useTestNG{} with useTestNG()
useDefaultListeners = true
}

// to pass specific commands from the command line
// using the -D switch for JVM system properties
systemProperties = System.getProperties()

// run max 2 tests in parallel
maxParallelForks = 2
}
“`

OK now lets get down and dirty with the code. We'll start with creating a base page object class to derive all the rest of our pages from.

  1. There will be a class each created under src/main/java and src/test/java directories. Delete the two classes.

  2. Create a new abstract Java class BasePageObject in src/main/java & paste the code below in it.

src/main/java/com/demo/POM/BasePageObject.java
“`java
package com.demo.POM;

import java.util.List;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;

public abstract class BasePageObject {
protected WebDriver driver;
protected WebDriverWait wait;

public BasePageObject(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(this.driver,30,10);

isLoaded();

}

/**
* Each page object must implement this method to return the identifier of a unique WebElement on that page.
* The presence of this unique element will be used to assert that the expected page has finished loading
* @return the By locator of unique element on the page
*/
protected abstract By getUniqueElement();

protected void isLoaded() throws Error{
//Define a list of WebElements that match the unique element locator for the page
List uniqueElement = driver.findElements(getUniqueElement());

    // Assert that the unique element is present in the DOM
    Assert.assertTrue((uniqueElement.size() > 0),
            "Unique Element \'" + getUniqueElement().toString() + "\' not found for " + this.getClass().getSimpleName());

    // Wait until the unique element is visible in the browser and ready to use. This helps make sure the page is
    // loaded before the next step of the tests continue.
    wait.until(ExpectedConditions.visibilityOfElementLocated(getUniqueElement()));

}

}
“`
The class has been made abstract as I want the class to only be extended and not instantiated. It contains a constructor that takes a WebDriver argument. It also verifies if the page is fully loaded by using the isLoaded() method. The isLoaded() method internally verifies if the user is on the correct page by calling the getUniqueElement() method which returns the locator of the element we use for such verification.

  1. For the purpose of this tutorial we'll be conducting tests on Stackoverflow.com – particularly the Home page and the Questions page. So, create two Java classes in the src/main/java folder:

src/main/java/com/demo/POM/pages/LandingPage.java
“`java
package com.demo.POM.pages;

import java.util.List;

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

import com.demo.POM.BasePageObject;

public class LandingPage extends BasePageObject {
@FindBy(css="div#menus")
List menuBar;

@FindBy(id="nav-questions")
WebElement questionLink;

@FindBy(id="nav-questions")
List questionsTab;

By uniqueElement = By.cssSelector("div#hmenus");

public LandingPage(WebDriver driver) {
super(driver);

PageFactory.initElements(this.driver, this);

}

@Override
protected By getUniqueElement() {
return uniqueElement;
}

public QuestionsPage clickQuestionsTab() {
    questionLink.click();
    return new QuestionsPage(driver);
}

public Boolean isQuestionsTabDisplayed() {
    return questionsTab.size() > 0;
}

}
“`

src/main/java/com/demo/POM/pages/QuestionsPage.java
“`java
package com.demo.POM.pages;

import java.util.List;

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

import com.demo.POM.BasePageObject;

public class QuestionsPage extends BasePageObject {
@FindBy(css=".youarehere #nav-questions")
WebElement youAreHere;

@FindBy(id="nav-users")
List usersTab;

public QuestionsPage(WebDriver driver) {
super(driver);

PageFactory.initElements(this.driver, this);

}

@Override
protected By getUniqueElement() {
return By.cssSelector(".youarehere #nav-questions");
}

public Boolean isUsersTabDisplayed() {
    return usersTab.size() > 0;
}

}

Both the classes extend from BasePageObject. And, override the getUniqueElement method. The various page elements are annotated with @FindBy. This allows for the Selenium PageFactory class to initialize the WebElements on the page. The PageFactory initialization happens in the constructor with the following line:
Java
PageFactory.initElements(this.driver, this);
“`
for more info on PageFactory click here

The initElements method takes a WebDriver instance & the class/pageobject to initialize as arguments.

Then LandingPage class contains a method clickQuestionsTab that navigates the user to the QuestionsPage by creating a new instance of the QuestionsPage (calling the constructor with the WebDriver instance & initializing the Questions page webelements).

  1. Now that we have the page objects ready, lets get cracking on creating tests for the pages. Just as we created a base page object, we will start by creating a base test class. This will allow us to define certain actions that need to be defined for each test – initiating the WebDriver, killing the driver at the end of the test, in the base test class. Create the base test class under src/main/test folder:

src/test/java/com/demo/POM/BaseTest.java
“`java
package com.demo.POM;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.LocalFileDetector;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;

public class BaseTest {

protected static final String WEBSERVER = System.getProperty("WEBSERVER", "http://stackoverflow.com/");
protected static final String BROWSER = System.getProperty("BROWSER", "firefox");
protected static final boolean REMOTEDRIVER = Boolean.valueOf(System.getProperty("REMOTEDRIVER", "false"));
protected static final String SELENIUMHOST = System.getProperty("SELENIUMHOST", "localhost");
protected static final int SELENIUMPORT = Integer.valueOf(System.getProperty("SELENIUMPORT", "4444"));

public static RemoteWebDriver driver;

@BeforeMethod (alwaysRun = true)
public void beforeMethod() {
driver.manage().window().maximize();
driver.get(WEB_SERVER);
}

@AfterMethod (alwaysRun = true)
public void afterMethod() {
driver.manage().deleteAllCookies();
}

@BeforeClass (alwaysRun = true)
public void beforeClass() throws MalformedURLException {
if (REMOTE_DRIVER) {
setupRemoteDriver();
driver.setFileDetector(new LocalFileDetector());
} else {
setupLocalDriver();
}
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
}

@AfterClass (alwaysRun = true)
public void afterClass() {
driver.quit();
}

private void setupLocalDriver() {
String path = "";
if (BROWSER.equals("firefox")) {
driver = new FirefoxDriver();
} else if (BROWSER.equals("chrome")) {
path = "lib/chromedriver";
if (System.getProperty("os.name").contains("Windows")) {
path = "lib/chromedriver.exe";
}
System.setProperty("webdriver.chrome.driver", path);
driver = new ChromeDriver();
} else if (BROWSER.equals("internetExplorer")) {
path = "lib/IEDriverServer.exe";
System.setProperty("webdriver.ie.driver", path);
DesiredCapabilities capabilities = DesiredCapabilities.internetExplorer();
capabilities.setCapability(InternetExplorerDriver.INTRODUCEFLAKINESSBYIGNORINGSECURITY_DOMAINS, true);
driver = new InternetExplorerDriver(capabilities);
} else {
throw new RuntimeException("Browser type unsupported");
}
}

private void setupRemoteDriver() throws MalformedURLException {
DesiredCapabilities capabilities;
if (BROWSER.equals("firefox")) {
capabilities = DesiredCapabilities.firefox();
} else if (BROWSER.equals("internetExplorer")) {
capabilities = DesiredCapabilities.internetExplorer();
capabilities.setCapability(InternetExplorerDriver.INTRODUCEFLAKINESSBYIGNORINGSECURITYDOMAINS, true);
} else if (BROWSER.equals("chrome")) {
capabilities = DesiredCapabilities.chrome();
} else {
throw new RuntimeException("Browser type unsupported");
}
driver = new RemoteWebDriver(
new URL("http://" + SELENIUM
HOST + ":" + SELENIUM_PORT + "/wd/hub"),
capabilities);
}
}
“`
The class defines the @BeforeClass & @AfterClass methods to initialize and destroy the driver instance &, @BeforeMethod & @AfterMethod methods to be executed before every test method. The class defines the various test properties such as the test URL, the browser to test on, whether to run the test on local machine or remote etc. The class also defines methods to setup a local driver (firefox/chrome/IE) & set up a remote driver instance. These will allow the user to vary the URL, browser & other parameters from the command-line.

  1. And, now to the actual test we are going to run. Create a TestNG test class under src/main/test.

src/test/java/com/demo/POM/test/ExampleTest.java
“`Java
package com.demo.POM.test;

import org.testng.Assert;
import org.testng.annotations.Test;

import com.demo.POM.BaseTest;
import com.demo.POM.pages.LandingPage;
import com.demo.POM.pages.QuestionsPage;

public class ExampleTest extends BaseTest{

public ExampleTest() {
super();
}

@Test
public void clickQuestionsTest() {
    LandingPage landingPage = new LandingPage(driver);
    QuestionsPage questionsPage = landingPage.clickQuestionsTab();
    Assert.assertTrue(questionsPage.isUsersTabDisplayed());
}

@Test
public void isLogoDisplayedTest() {
    LandingPage landingPage = new LandingPage(driver);
    Assert.assertTrue(landingPage.isQuestionsTabDisplayed());
}

}
“`

And that is it!! We are now ready to run the tests!

  1. To run the tests from the command prompt, open the command prompt and navigate to the project folder. Run the following command to launch the tests:
    cmd
    gradle clean test

    'clean' cleans out the build directory of any previous builds while 'test' runs the unit/integration/functional tests in the project.

The above command will launch the browser and, you should be able to see the test steps being performed. On successful completion of the tests the command prompt should display the following output:
“`cmd
:clean UP-TO-DATE
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

BUILD SUCCESSFUL

Total time: 1 mins 17.821 secs
“`

The complete project and code can be got from Page Object Pattern.

Take Aways

As you would have observed with the code above, the creation of class corresponding to each and every page in the AUT allows us to reuse the same class across various tests. In OOPS terminology each Page Object class encapsulates the elements and behaviors of the particular page in question. What it doesn't mean is that a Page Object class to encapsulate all the behaviors exhibited on the page. We can also model a page object with only a partial set of behaviors. This also makes the automation code modular in nature.

Not only is the code reusable, it is maintainable as well. In case the test case related to the Landing Page fails, I now know I need to update my code in the Landing Page class only and no where else. If I need to add a new method for functionality related to the Landing Page, I will do so in the class encapsulating the landing page.

Leave a Reply

Your email address will not be published. Required fields are marked *