Design Patterns In Selenium Automation Part1 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 we:

  • create classes that model/represent the various pages of the AUT,
  • 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,
+ Groovy,
+ 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.)

Step 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:

gradle init --type groovy-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/groovy & 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)

Step 2

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

// Apply the plugin to add support for groovy, eclipse and intellij.
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.3.10'
    compile 'org.seleniumhq.selenium:selenium-java:2.45.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 omit the commands between the {}
    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.

Step 3

Delete src/main/groovy/Library.groovy & src/main/groovy/LibraryTest.groovy.

Step 4

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

src/main/groovy/com/demo/POM/BasePageObject.groovy

package com.demo.POM
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

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

    BasePageObject(WebDriver driver) {
        this.driver = driver
        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 def isLoaded() throws Error{
        //Define a list of WebElements that match the unique element locator for the page
        List<WebElement> 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.class.simpleName}")

        // 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 we want the class to be extended from 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.

Step 5

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/groovy/com/demo/POM/pages/HomePage.java

package com.demo.POM.pages
import com.demo.POM.BasePageObject
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

class HomePage extends BasePageObject {
    @FindBy(css="div#menus")
    List<WebElement> menuBar;

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

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

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

    HomePage(WebDriver driver) {
        super(driver)
    }

    @Override
    protected By getUniqueElement() {
        return By.cssSelector("div#hmenus")
    }

    def QuestionsPage clickQuestionsTab() {
        questionLink.click()
        return PageFactory.initElements(this.driver, QuestionsPage.class)
    }

    def isQuestionsTabDisplayed() {
        return questionsTab.size() > 0
    }

}

src/main/groovy/com/demo/POM/pages/QuestionsPage.groovy

package com.demo.POM.pages
import com.demo.POM.BasePageObject
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.support.FindBy

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

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

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

    @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 method responsible for creating the page object with the following line:

PageFactory.initElements(this.driver, PageObject.class)

for more info on PageFactory click here

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

Then HomePage class contains a method clickQuestionsTab that navigates the user to the QuestionsPage by creating a new instance of the QuestionsPage (initializing the Questions page WebElements using PageFactory).

Step 6

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/groovy/com/demo/POM/BaseTest.groovy

package com.demo.POM
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

import java.util.concurrent.TimeUnit

class BaseTest {

    protected static final String WEB_SERVER = System.getProperty("WEB_SERVER", "http://stackoverflow.com/")
    protected static final String BROWSER = System.getProperty("BROWSER", "firefox")
    protected static final boolean REMOTE_DRIVER = Boolean.valueOf(System.getProperty("REMOTE_DRIVER", "false"))
    protected static final String SELENIUM_HOST = System.getProperty("SELENIUM_HOST", "localhost")
    protected static final int SELENIUM_PORT = Integer.valueOf(System.getProperty("SELENIUM_PORT", "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() {
        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.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true)
            driver = new InternetExplorerDriver(capabilities)
        } else {
            throw new RuntimeException("Browser type unsupported")
        }
    }

    private void setupRemoteDriver() {
        DesiredCapabilities capabilities
        if (BROWSER.equals("firefox")) {
            capabilities = DesiredCapabilities.firefox()
        } else if (BROWSER.equals("internetExplorer")) {
            capabilities = DesiredCapabilities.internetExplorer()
            capabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, 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.

Step 7

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

src/test/groovy/com/demo/POM/test/ExampleTest.groovy

package com.demo.POM.test

import org.openqa.selenium.support.PageFactory
import org.testng.Assert
import org.testng.annotations.Test

import com.demo.POM.BaseTest
import com.demo.POM.pages.HomePage
import com.demo.POM.pages.QuestionsPage

class ExampleTest extends BaseTest{

    ExampleTest() {
        super()
    }

    @Test
    public void clickQuestionsTest() {
        HomePage landingPage = PageFactory.initElements(this.driver, HomePage.class)
        QuestionsPage questionsPage = landingPage.clickQuestionsTab()
        Assert.assertTrue(questionsPage.isUsersTabDisplayed())
    }

    @Test
    public void isLogoDisplayedTest() {
        HomePage landingPage = PageFactory.initElements(this.driver, HomePage.class)
        Assert.assertTrue(landingPage.isQuestionsTabDisplayed())
    }
}

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

Step 8

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:

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:

: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 *