Geb Functional Testing Unleashed

Craig Atkinson | Principal Consultant | Object Partners

About Me

  • Craig Atkinson
  • Principal Consultant, Object Partners (OPI)
  • Using Geb 3 years
  • Very minor Geb contributor
  • @craigatk1
  • craig.atkinson@objectpartners.com

Agenda

  • Quick Geb Intro
  • Page Objects
  • Built-in test helpers
  • Cross-browser testing
  • Running tests in parallel
  • Remote controls
  • Mock 3rd-party services

Geb Intro

What is Geb?

  • Groovy browser testing framework built upon Selenium
  • jQuery-style selectors
  • Built-in page objects
  • and much more!

What can you test with Geb?

  • Any web application
  • App doesn't need to be written in Groovy

Simple example test


  go "http://www.google.com"
  
  $("input", name: "q").value("geb")
  $("button", name: "btnG").click()
  
  waitFor { $("#search").displayed }
  
  $("h3.r").first().click()
  
  waitFor { title == "Geb - Very Groovy Browser Automation" }
  

Quick demo

Selector syntax


 $("#by-id")
 
 $(".by-class")
 
 $("div", name: "by-attribute")
 
 $("div", text: "By text")
 

Full list of selectors in manual

Easy to read and maintain?

  • Tests with embedded page structure (HTML) are less readable
  • Multiple places to update if page structure changes
  • Can we abstract page structure out of tests?

Page Objects

Page Object pattern

  • Abstract page-specific details into helper classes (Page Objects)
  • Re-used across tests
  • Single point of maintenance
  • Tests become easier to read

Test with page objects


  to GoogleHomePage
  
  searchBox = "Geb"
  
  searchButton.click()
  
  searchResultsLinks.first().click()
  
  assert at(GebHomePage)
  

Page object


  class GoogleHomePage extends geb.Page {
    static url = "http://www.google.com"
    
    static content = {
      searchBox { $("input", name: "q") }
      
      searchButton { $("button", name: "btnG") }
      
      searchResultsLinks(wait: true) { $("h3.r") }
    }
  }
  

What's in a Geb Page Object?

  • Elements on page and how to find them
  • How to verify test is currently on page
  • URL to go directly to page
  • Helper methods to simplify page interactions

Elements on page

  • content block
  • Defines elements on the page that tests will interact with
  • Includes selectors that tell Geb how to find the element on the page

Content Block Example


  class IdeaCreatePage extends geb.Page {
  
    static content = {
    
      titleField { $("#title") }
      descriptionField { $("#description") }
      createButton { $("#create") }
      
    }
  }
  

Verify test at current page


  class IdeaCreatePage extends geb.Page {
  
    static at = { $("div#create-idea").displayed }
    
  }
  

Go directly to page


  class IdeaCreatePage extends geb.Page {
  
    static url = "idea/create"
    
  }
  


  IdeaCreatePage ideaCreatePage = to(IdeaCreatePage)
  

Change Page when Clicking

  • Geb keeps track of the current page
  • Tell Geb when the page changes

  static content = {
  
    createButton(to: IdeaShowPage) { $("input#create") }
    
  }
  


  void clickCreateButton() {
    createButton.click()
    
    assert browser.page.class == IdeaShowPage
  }
  

Multiple Possible Pages?

  • Use a list of possible page classes as to value
  • Geb uses at check to determine resulting page

  static content = {
  
    createButton(to: [IdeaShowPage, IdeaErrorPage]) { $("input#create") }
    
  }
  

Alternative approach to page objects

Standard page object

Standard: Geb delegates method calls to current page object

Test with standard page objects


  to HomePage
  loginButton.click()
  
  username = "user1"
  password = "password1"
  submitButton.click()
  
  // What page is the test on now?
  
  // What fields are on the current page?
  

Strongly-typed page objects

Test uses reference to current page object

With typed pages


  HomePage homePage = to HomePage
  
  LoginPage loginPage = homePage.clickLoginButton()
  
  DashboardPage dashboardPage = loginPage.login("user1", "password1")
  

Chain page calls


  HomePage homePage = to HomePage
  
  DashboardPage dashboardPage = homePage
    .clickLoginButton()
    .login("user1", "password1")
    

Typed page characteristics

  • Page objects have methods that resemble user actions
  • Methods that change page return instance of new page

Typed home page


  class HomePage extends geb.Page {
    static content = {
      loginButton(to: LoginPage) { $("#loginButton") }
    }
    
    LoginPage clickLoginButton() {
      loginButton.click()
      
      return browser.page
    }
  }
  

Typed login page


  class LoginPage extends geb.Page {
    static content = {
      usernameField { $("#username") }
      passwordField { $("#password") }
      submitButton(to: DashboardPage) { $("#submit") }
    }
    
    DashboardPage login(String username, String password) {
      usernameField.value(username)
      passwordField.value(password)
      submitButton.click()
      
      return browser.page
    }
  }
  

Data type conversion


  class DependentPage extends geb.Page {
    static content = {
      firstNameField { $("#firstName") }
      lastNameField { $("#lastName") }
      
      birthdayDayField { $("#birthdayDay") }
      birthdayMonthField { $("#birthdayMonth") }
      birthdayYearField { $("#birthdayYear") }
    }
    
    void addSpouse(String firstName, String lastName, LocalDate birthday) {
      firstNameField.value(firstName)
      lastNameField.value(lastName)
      
      birthdayDayField.value(birthday.dayOfMonth.toString())
      birthdayMonthField.value(birthday.monthOfYear.toString())
      birthdayYearField.value(birthday.year.toString())
    }
  }
  

Typed page objects summary

  • Little additional work to write page objects
  • Save more time writing (and later reading) test code

Built-in Test Helpers

Waiting support

  • Dynamic elements and content common with Ajax, Javascript MVC, etc.
  • Can also wait for data changes (new DB record created, etc.)
  • waitFor method available in tests and page objects

waitFor examples


  waitFor {
    $("div.alert").displayed
  }
  
  waitFor {
    $("div.message").text() == "Update successful"
  }
  

Complex UI elements

Sliders, date pickers, etc.

Mouse interactions

  • Click and drag slider with mouse

  static content = {
    ratingSliderHandle { $(".ui-slider-handle") }
  }
  
  void moveRatingSlider(Integer rating) {
    // Slider is 400 pixels wide and starts at 1,
    // so each notch above 1 is 100 pixels apart
    Integer numPixelsX = (rating - 1) * 100
    
    interact {
      clickAndHold(ratingSliderHandle)
      moveByOffset(numPixelsX, 0)
      release()
    }
  }
  

Mouse interaction demo

Use keyboard

  • Send keystokes with left-shift operator <<

  $("#myInputField") << "value"
  

  $(".ui-slider-handle") << Keys.ARROW_RIGHT
  

Keyboard demo

Execute raw Javascript

  • Last-ditch effort with complex UI controls
  • All Navigator elements have a 'jquery' field to run JS

Show hidden element


  static content = {
    hiddenLink { $("#hiddenLink") }
  }
  
  void clickHiddenLink() {
    hiddenLink.jquery.show()
    
    hiddenLink.click()
  }
  

Cross-Browser Testing

Available browsers

  • Any browser with a Selenium/Webdriver library
  • Real: Firefox, Chrome, IE, Safari, etc.
  • Simulated: HtmlUnit, PhantomJS

Configuring browser in Geb

  • Browser Selenium driver dependency
  • Additional OS-specific browser driver
  • Section in GebConfig.groovy
  • Pass environment param to Geb

Selenium driver dependencies


  def seleniumVersion = "2.46.0"
  
  test "org.seleniumhq.selenium:selenium-support:${seleniumVersion}"
  test "org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}"
  test "org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}"
  test "org.seleniumhq.selenium:selenium-ie-driver:${seleniumVersion}"
  

Operating system driver

  • Firefox doesn't need one
  • Chrome, Internet Explorer need local driver executable
  • Manually download, install, and configure
  • Or ...

Automatic download based on OS

Chrome GebConfig Example


  environments {
    chrome {
      String version = "2.14"
      String zipFileName
      String execFileName
      
      if (Platform.current.is(Platform.MAC)) {
        zipFileName = "chromedriver_mac32.zip"
        execFileName = "chromedriver"
      } else if (Platform.current.is(Platform.LINUX)) {
        zipFileName = "chromedriver_linux32.zip"
        execFileName = "chromedriver"
      } else if (Platform.current.is(Platform.WINDOWS)) {
        zipFileName = "chromedriver_win32.zip"
        execFileName = "chromedriver.exe"
      }
      
      String url = "http://chromedriver.storage.googleapis.com/${version}/${zipFileName}"
      File localExecutable = downloadDriver(url, execFileName, 'zip')
      System.setProperty('webdriver.chrome.driver', localExecutable.absolutePath)
      
      driver = { new ChromeDriver() }
    }
  }

Pass geb.env parameter


  grails -Dgeb.env=chrome test-app functional:
  

Parallel testing

Parallel testing requirements

  • Build tool that supports parallel testing (Gradle, etc.)
  • Tests can't modify any shared data
  • Safest to have each test set up its own data

Parallel testing example

  • Using Gradle
  • Start app using Grails wrapper
  • Run 2 tests simultaneously
  • Shut down Grails app
  • Demo

Groovy Remote Control

What are remote controls useful for?

  • Setting up data specific to a test
  • Grabbing data after test for verification
  • Retrieving & setting up data in mock services (stay tuned)

How does it work?

Data setup example


  Idea createIdea(String title, String description) {
    RemoteControl remote = new RemoteControl()
    
    remote {
      Idea idea = new Idea(
          title: title,
          description: description
      )
      
      idea.save()
    }
  }
  

  List ideas = (1..5).collect { i ->
    ideaRemoteControl.createIdea("Title ${i}", "Description ${i}")
  }
  

Data verification example


  Idea findByTitle(String title) {
    RemoteControl remote = new RemoteControl()
    
    remote {
      Idea.findByTitle(title)
    }
  }
  

Mock third-party services

Proliferation of Third-party services

  • Email, payment processing, storage, address verification, etc.
  • Great for productivity, but can complicate testing
  • Don't want our tests dependent on services out of our control

Solution?

  • Mock call/response from external services
  • Our tests not dependent on external service
  • Tests have tight control over service responses

Environment-specific Dependency Injection

  • If using DI framework like Spring, Guice, etc.
  • Create mock version of code that calls external service
  • Use DI to replace code with mock version during functional tests
  • Setup and verify data from mock services in tests

Example service to mock

PatentService sends ideas to patent office


  class PatentService {
  
    def sendToPatentOffice(Idea idea) {
      // Send the idea to the real patent office
      
    }
    
  }
  

Mock Patent Service


  class PatentServiceMock extends PatentService {
    List ideasSentToPatentOffice = []
    
    @Override
    def sendToPatentOffice(Idea idea) {
      ideasSentToPatentOffice << idea
    }
  }
  

Use mock patent service

  • Replace PatentService instance with mock version
  • In Grails, grails-app/conf/spring/resources.groovy

  beans = {
  
    if (Environment.current == Environment.TEST) {
      // Override the PatentService with our mock version when running tests
      patentService(PatentServiceMock)
      
    }
  }
  

Test that hits mock service


  def 'should submit idea to patent office'() {
    given:
    IdeaRemoteControl ideaRemoteControl = new IdeaRemoteControl()
    
    IdeaCreatePage ideaCreatePage = to(IdeaCreatePage)
    
    String ideaTitle = "Patentable Idea"
    String ideaDescription = "This idea is going to change the world"
    
    when:
    IdeaShowPage ideaShowPage = ideaCreatePage.createIdea(ideaTitle, ideaDescription)
    ideaShowPage = ideaShowPage.submitIdeaToPatentOffice()
    
    then:
    Idea newIdea = ideaRemoteControl.findByTitle(ideaTitle)
    
    List ideasSubmittedToPatentOffice = ideaRemoteControl
        .findIdeasSubmittedToPatentOffice()
    assert ideasSubmittedToPatentOffice*.id.contains(newIdea.id)
  }
  

Access mock service in remote controls


  class IdeaRemoteControl {
    RemoteControl remote = new RemoteControl()
    
    List findIdeasSubmittedToPatentOffice() {
    
      remote {
        return ctx.patentService.ideasSentToPatentOffice
      }
      
    }
  }
  

Wrapping up

Contributing to Geb

Resources

Q&A

  • @craigatk1
  • craig.atkinson@objectpartners.com