4 minute read

Background / The Problem

In the previous example we stepped through a very basic guide to creating your first scala, cucumber and webDriver test. This may have been enough to get you started, but there’s a lot of room for improvement. If you have struggled to build upon the example outlined in part-one, and would like to build a maintainable framework rather than just a test, read on.

The Solution

We abstract all page level actions, and elements into Page Objects and utilize selenium web-diver’s PageFactory. For example each page of your application has it’s own ‘page’ class which lists all locators and common methods related to these pages. This way when your app code changes causing test failures you will only have to update your test code in one place.

Example

In this example we will expand upon my previous post and upgrade it by abstracting the webDriver instance and create page-objects.

Driver Class

In order to use a single instance of webDriver we abstract the driver and wrap it within an object, often called a webDriver singleton. The purpose of this is to control object creation, limiting the number of objects to one.

object Driver {
 
   var driver: WebDriver = _
 
   def getDriver: WebDriver = {
     if (driver == null) {
     Firefox.firefoxProfile.setAcceptUntrustedCertificates(true)
     driver = Firefox.webDriver
    }
    driver
   }

}

The Base class

Next we implement a base-class.

object Base {
 
  val BASE_URl     = "https://wordpress.com"
 
  val LOAD_TIMEOUT = 30
 
  val REFRESH_RATE = 2
 
}
 
abstract class Base[T] {
 
  lazy val webDriver = getDriver
 
  def initPage(clazz: Class[T]): T = {
    val page = PageFactory.initElements(getDriver, clazz)
    val pageLoadCondition = page.asInstanceOf[Base[T]].getPageLoadCondition
    waitForPageToLoad(pageLoadCondition)
    page
  }
 
  def navigateToPage(url: String) {
    getDriver.navigate().to(BASE_URl + url)
  }
 
 private def waitForPageToLoad(pageLoadCondition: ExpectedCondition[WebElement]) {
    val wait = new FluentWait(getDriver).withTimeout(LOAD_TIMEOUT, TimeUnit.SECONDS)
      .pollingEvery(REFRESH_RATE, TimeUnit.SECONDS)
    wait until pageLoadCondition
  }
 
  def getPageLoadCondition: ExpectedCondition[WebElement]
 
}

The Base object and Base class are common to all pages, including methods and page level helpers that each page-object inherit.

The base object is fairly self explanatory, listing fields for your base URL, load timeout etc.

The Base class includes the Base object and contains a method initPage, which will pass an instance of webDriver from the getDriver method created above in order to initialize a page-object (for example Login page) and any WebElement fields that are listed within that page-object.

Next we have a waitForPageToBeLoaded method, which waits for a condition, for example an element to be visible on page before proceeding. This condition will be created from within our page-objects (more below).

The Page Class

class Login extends Base[Login] {
  def initPage: Login = {
    navigateToPage("/login")
    new Login().initPage(classOf[Login])
  }
 
  @FindBy(id = "user_login")
  var loginField: WebElement =_
 
  @FindBy(id = "user_pass")
  var passwordField: WebElement =_
 
  @FindBy(id = "wp-submit")
  var loginButton: WebElement =_
 
  @FindBy(id = "login_error")
  var loginError: WebElement =_
 
  override def getPageLoadCondition: ExpectedCondition[WebElement] = {
    ExpectedConditions.visibilityOf(loginField)
  }
 
  def login(userName: String, password: String) {
    loginField.sendKeys(userName)
    passwordField.sendKeys(password)
    loginButton.click()
  }
}

The above code snippet is a simple example of the WordPress Login page.

  • First of all we name our page class, naming it by the page name of your application under test. In this case the first page that we navigate to on the WordPress website is the Login page, hence why I have named the class Login.
  • The Login page extends (inherits) the Base class created above. The Login page is a subclass of Base, i.e. Base[Login] (more information on Type and polymorphism here) This way the initPage method and wait condition are executed when the Login page is initialized.
  • Next we use PageFactory to declare some fields, listing page level elements (for example submit button).
  • In order to wait for this page to be loaded, we create our page load condition method. In this case we are waiting for the visibility of the login field. Next we create any common methods related to the page, in this case we have a common login method, passing userName and password as String type parameters.

The Step Definitions Class

Once we have created our page objects, we are now ready to access them from within our Step Definitions.

The page-objects must declared with a lazy val.

For example:

class MyFirstPageObjectTestSteps extends ScalaDsl with EN {
 
  lazy val wpLogin = new Login().initPage
  lazy val wpHome  = new Dashboard().initPage
}

The reason why we use lazy val is; we only want to initialize a page when we need to. Otherwise it will cause tests to fail as the pages will be initialized each time we execute a test, or a step definitions class is used due to waiting for page level elements in our page classes (getPageLoadCondion method). Essentially pages should only accessed if, and when we require them.

Accessing page-objects from Step Definitions

class MyFirstPageObjectTestSteps extends ScalaDsl with EN {
 
  lazy val wpLogin = new Login().initPage
  lazy val wpHome  = new Dashboard().initPage
 
  Given( """^I login with (valid|invalid) credentials to Add WordPress.com blog$""") { (negate: String) =>
    if (negate.equals("valid")) {
      wpLogin.login("info@example.co.uk", "valid")
    } else {
      wpLogin.login("info@example.co.uk", "invalid")
    }
  }
 
  When( """^I (should|should not) be logged in$""") { (negate: String) =>
    if (negate.equals("should")) {
      assertTrue("Wordpress Dashboard not displayed", wpHome.blogTitle().contains("infoatrubygemtsl"))
    } else {
      assertTrue("Error message was not displayed", wpLogin.loginErrorMessage().equals("ERROR: The password you entered for the email or username info@rubygemtsl.co.uk is incorrect. Lost your password?"))
    }
  }
}

Summary

Following the above example you should be well on your way to building a robust, well maintainable test framework. It is important to remember that the more time invested in building maintainable frameworks, the better. A reliable test suite will enable tests to be executed on demand/periodically via Continuous Integration in the run up to a release, as well as freeing a tester use their knowledge and testing skills execute exploratory based testing.

The natuaral next steps for a useful and maintainable test pack would be to extend the driver class to cover cross-browser and mobile web based testing, as well as introduce some of the features of cucumber such as Before/After hooks.