geb

browser automation with pleasure

Stefan Hildebrandt / @hildebrandttk

me

you?

this slide deck

more details in speaker notes
open popup with [s].

material for this workshop

examples

geb - gebish.org

  • Basis ist der Selenium WebDriver
  • Bildet eine Abstraktionsebene für das Selenium API
  • Bietet Strukturierungshilfen in Form von Page- und Module-Objekten
  • Stellt weitere Utils bereit
  • Ist in Groovy implementiert
  • Version 2.0 gerade erschienen
  • Online Handbuch: Book of Geb

Groovy?

methodMissing

... Ideal für DSLs

JVM

... nahtloser Übergang zur Applikation

Groovy!

Groovy - Klasse

package tk.hildebrandt.geb

class Example {
}

Groovy - Variablen & Methoden

class Example {
   def foo;
   String bar;

   def doFoo(def foo){
      def bar
   }

   boolean doBar(){
      bar
   }
}

Groovy - Collections

@Test
void callWithList(){
   withList([1, 1, 2, 3, 5])
}

void withList(def list) {
   list.each {
      entry ->
         println "${entry}"
   }
   list.eachWithIndex {
      entry, index ->
         println "${index}: ${entry}"
   }
}

Groovy - spread operator

void callWithList(){
   withList([1, 1, 2, 3, 5])
}

void withList(def list) {
      println list*.toString()
}

Groovy - Maps

void callWithMap() {
   withMap(test: '123', name: '324')
}

void withMap(def map) {
   map.each {
      key, value ->
         println "${key}: ${value}"
   }
}

Groovy - Closures

void callWithClosure() {
   assert withClosure {
      final boolean result = 1 == 1
      println "within closure ${result}"
      result
   }
}

def withClosure(Closure<Boolean> closure) {
   closure.call()
}

Power Assertions

final String test = "test"
assert "test" == test && "test2" == test

Ausgabe

assert "test" == test && "test2" == test
              |  |    |          |  |
              |  test false      |  test
              true               false
geb

Browser

  • Referenz zum WebDriver
  • Aktuelle Seite
  • Reporting
Browser.drive {
   go "http://google.com/"
}
geb

jQuery-Style-Selector

$("input", name: "q")
$("li.g", 0).find("a.l")
$("p", text: "Hans")
$("p", endsWith: "aus")
geb

Navigator

  • Ist ein Wrapper um Selenium WebElement
  • Ausgangspunkt für weitere Suche
  • Werte beziehen: value() / text()
  • Werte setzen: value(...)
  • Attribute lesen: @src, @href
  • Aktionen ausführen: click(), ...
$("input", name: "q").value("wikipedia")
$("select").value("wikipedia", "google")
assert $("h1").text() == "Wikipedia"
assert $("h1").parent().@src.contains("wikipedia")
$("button").click()
geb

Selenium By.*-Selectors

  • in some cases faster
$(By.id("some-id"))
$(By.className("some-class"))
$(By.xpath('//p[@class="xpath"]'))
geb

the $()-Function

  1. String as css-selector
  2. index selected from a list of results
  3. map of parameters
    • href: startsWith('https')
    • src: endWith('.png')
geb

css-selector

  • rich support of selectors
Geb

Scripting

@Test
public void testSearchWithPageAndModule(){
   go 'https://www.bing.com'
   assert title == 'Bing'
   $('input', name: 'q').value("wikipedia")
   $(By.id('sb_form_go')).click()
   waitFor { title.endsWith(' - Bing') }
   assert $('li.b_algo a', 0).text() == "wikipedia.de - Wikipedia, die freie Enzyklopädie";
}

lesson 1: Scripting

  1. Open lmis.de
  2. search for "java"
  3. check first result title
scripting

Problems

  • spaghetti code
  • bad identifiers
  • no possibility of reuse of
    • identities
    • navigation/access logic
  • but: typical result
scripting

common problem:

how find existing code

scripting

solution

  • conventions
  • maps
  • how to get one for all?

page object pattern

page object pattern

mapping

page object pattern

Pages

  • every logical page is an class
    • hierarchies are possible
  • fields for page elements
page object pattern

actions

  • methods for actions
  • unit testing: fine grained
    • fillA
    • clickB
    • getC
  • functional testing:
    • searchFor
    • getResultByIndex
page object pattern

Navigation

  • by method return types
    • goToLoginPage(): LoginPage
    • searchFor(query:String): ResultPage
page object pattern

fluent api

  • every method returns type for next action
page object pattern

assertions

  • as method of a page
  • self describing name
page object pattern

return values

  • java lamdas are limited
    • value objects
    • groovy closures
    • other jvm languages
page object pattern

closures

  • Method on page
    GoogleResultsPage doSomething(Closure closure) {      .
       closure.call(this)
       this
    }
  • call
    List<GoogeResultModule> results;
    googleResultsPage
      .doSomething (
        { GoogleResultsPage page ->
          results = page.listResults()
    })
      .goToNextPage()
page object pattern

ajax and async updates

  • a mass of complexity
  • hide all async calls in page objects
page object pattern

facade pattern

  • build an own interface-facade to remote system
  • domain specific language over html interface
Geb

Page

class BingStartPage extends Page {
   static url = 'https://www.bing.com'

   static at = { title == 'Bing' }

   static content = {
      searchField { $('input', name: 'q') }
      searchButton(to: BingResultsPage) {
         $(By.id('sb_form_go'))
      }
   }
}
geb page

test

@Test
public void testPage(){
   to BingStartPage
   assert at(BingStartPage)
   searchField.value("wikipedia")
   searchButton.click()
   waitFor { at BingResultsPage }
   assert searchFirstResultLink.text() == "wikipedia.de - Wikipedia, die freie Enzyklopädie";
}
geb page

at

  • executed by geb to check if on page
  • every line is an assert
  • other formats require explicit asserts
static at = { title == 'Bing' }
geb page

url

  • used by geb's to directive
  • absolute
  • relative to baseUrl defined in GebConfig.groovy
static url = 'https://www.bing.com'
geb page

content dsl

  1. name
  2. parameters (optional)
  3. closure for content
    • Navigator
    • any other object type
static content = {
   searchField { $('input', name: 'q') }
   searchFirstResultLink { $('li.b_algo a', 0) }
   searchButton(to: BingResultsPage) { $(By.id('sb_form_go')) }
}
lesson 2

Page

  1. create home-page object
  2. navigate to start page
  3. search for java
  4. create result page object
  5. check title of first result
Geb

Page - Methods

  • add higher level methods on pages
  • hint navigation by return types

public BingResultsPage search(String query){
   searchField.value(query)
   searchButton.click()
   waitFor { browser.at BingResultsPage }
   browser.page(BingResultsPage)
}

lesson: page with method

  • create a search method on your page
  • use the method in your test
  • refactor page and test
Geb

Page - fluent api

  • requires assert methods
  • data retrieval as closure
Geb

Page - assert method

   BingResultsPage assertFirstResultLinkText(String expected) {
      assert searchFirstResultLink.text() == expected
      this
   }

lesson: assert method

  • remove all page content from your test
  • create a fluent api on your pages
  • refactor page and test
Geb

Page - closure

  T doOnPage(Closure closure) {
    Closure cloned = closure.clone()
    cloned.delegate = browser
    cloned.resolveStrategy = Closure.DELEGATE_FIRST
    cloned.call()
    this
  }
public void testDoOnPage() {
    String link = null
    to(BingStartPage)
            ...
    .doOnPage ({
      link = searchFirstResultLink.@href
    })
  }

lesson: closure

  • retrieve the url of the first result to a variable in your test
  • preserve the fluent api
  • refactor page and test

Async!

  • example: ajax page update after click
    • user 'starts' action
    • js sends request to server
    • user action is finished
    • js updates dom with result after server answer
Geb Async

content dsl

static content = {
   dynamicallyAdded(wait: true) { $("p.dynamic") }
   dynamicallyAddedOrNot(wait: true, required: false)
   { $("p.dynamicButOptional") }
   dynamicallyAddedOrNot(wait: "ajax", required: false)
   { $("p.dynamicButOptional") }
}
//Exception nach dem Wait-Timeout
   assert dynamicallyAdded.text() == "I'm here now"
   assert dynamicallyAddedOrNot.text() == "I'm here now"
   //nach dem Wait-Timeout ist das Value null
   || dynamicallyAddedOrNot.value() == null

Geb: Async mit waitFor

waitFor { title.endsWith("Google Search") }
   waitFor { at GoogleResultsPage }
   waitFor 30 { at GoogleResultsPage }
   waitFor ("searchResult", { at GoogleResultsPage })
geb

waiting

  • true: default timeout
  • 30: timeout in seconds
  • 30, 1: timeout in seconds, retry interval
  • 'slow', 'fast', 'search': named timing intervals from GebConfig.groovy

lesson: async page update

  1. bing search
    • enter search term
    • wait for suggestions
    • list and assert suggestions
page object pattern

smaller parts

  • repeating elements
  • js enhanced html (jQuery UI, Kendo UI, extjs, ...)
  • complex web frameworks (angular/react/gwt/vadin/...)
  • complex jsf components (richfaces/primefaces/butterfaces)
  • selenium way:
    • UtilClasses
geb

existing modules

  • for form elements
    • text
    • textarea
    • select
    • checkbox
    • ...
  • some specific methods
   static content = {
      searchField { $('input', name: 'q').module(TextInput) }
   }

lesson: existing modules

  • use module for search field
  • refactor page and test
geb

module list

  • like page
  • locators relative to base
class BingResultModule extends Module {
   static content = {
      header { $('h2') }
      link { $('h2>a') }
   }
}
content dsl

parameter

static content = {
   searchField { nameParameter -> $('input', name: nameParameter) }
}
geb

module list

  static content = {
      results { $('li.b_algo').moduleList(BingResultModule)}
      searchFirstResultLink { $('li.b_algo a', 0) }
   }

   BingResultsPage assertFirstResultLinkText(String expected) {
      assert results.get(0).link.text() == expected
      this
   }

lesson: module list

  • create a module for the result rows
  • support header, url, content, images as getter
  • create a tests that checks all attributes

lesson: complex module

  • create a module for PrimeReact PickList
    • Lists elements on the left and right with image and text
    • move elements by name from left to right
  • create a tests that checks all attributes
geb

jquery

  • execute javascript within the browser
    • use the jquery-attribute on navigator
  • powerfull
  • slow and sometimes less stable
   BingResultsPage search(String query){
      searchField.setQuery(query)
      searchButton.jquery.click()
      waitFor { browser.at BingResultsPage }
      browser.page(BingResultsPage)
   }

Geb: GebConfig.groovy

  • groovy ConfigSlurper file
  • named closures
  • no frame

browser instantiation

driver = "ie"
driver = "chome"
driver = {
   def ffDriver = new FirefoxDriver()
   ffDriver.manage().window().maximize()
   return ffDriver
}

timing presets

waiting {
   timeout = 10
   retryInterval = 0.5
   presets {
      ajax {
         timeout = 3
         retryInterval = 0.5
      }
   }
}

environments

environments {
   'phantomjs' {
      driver = {
         final capabilities = new DesiredCapabilities()
         capabilities.setCapability("javascriptEnabled", true)
         final phantomJSDriver = new PhantomJSDriver(capabilities)
         phantomJSDriver
            .manage()
            .window()
            .setSize(new Dimension(1028, 768))
         return phantomJSDriver
      }
   }
}
mvn -Dgeb.env=phantomjs test

browser support WebDriver

  • chrome with chrome driver
  • firefox with gecko driver
  • edge explorer with iedriver
  • internet explorer with iedriver
  • opera with opera driver
  • HtmlUnit

lesson

  • run tests with chrome and phantomjs
  • look for differences

Reporting

  • automatic reporting on test end
    • every page change is also support
  • use different base class:
    • geb.spock.GebReportingSpec
    • geb.junit4.GebReportingTest
    • ...
  • explicit by: browser.report('<name>')
  • report location is specified by "reportsDir" in GebConfig.groovy

lesson

  • add report support
  • try to debug a phantom js run with a simple error

file download

  • no support by WebDriver
    • some hack by using download-dialog and sendKeys
      • browser dependent and fragile
  • geb transfers session state and referer to a HttpURLConnection
  • download without browser

interact

  • support for complex interactions on webdriver
    • drag and drop
    • multi key actions
  • alternative to selenium actions

interact

interact {
   keyDown(Keys.SHIFT)
   doubleClick($('li.clicky'))
   keyUp(Keys.SHIFT)
}

interact

interact {
   clickAndHold($('#element'))
   moveByOffset(400, -150)
   release()
}
interact {
   dragAndDropBy($('#element'), 400, -150)
}

Javascript

  • browser.js.someGlobalFunction('anArg')
  • $('selector').jquery.click()

Frames und Windows

  • withFrame
  • withWindow
  • withNewWindow

Frames und Windows

withWindow('MyNewTab') {
   $('#MyNewTabCloseButton').click()
}

additional topics

  • cucumber? / spoc
  • use geb from ???
  • headless test execution
  • load / performance tests with jMeter

Spoc

Behavior Driven Development

Spoc

  • Namenswahl frei
  • Struktur
    1. Vorbedingung (given)
    2. Aktion (when)
    3. Prüfungen (then)
  • "then"-Bock enthält implizite assertions

Spoc

class ExamplePageAndModuleSpec extends geb.spock.GebSpec {

   def "test search with page and module"() {
      when: {
         to GoogleStartPage
         search.searchField.value("wikipedia")
         waitFor { at GoogleResultsPage }
      }
      then: {
         searchFirstResultLink.text() == "Wikipedia"
      }
   }
}

Aufgabe Spoc

  • Erstellen einer Spec zur Suche mit Spock

Stefan Hildebrandt - consulting.hildebrandt.tk

  • Beratung, Coaching und Projektunterstützung
  • Java EE
  • Buildsysteme gradle und maven
  • Testautomatisierung
  • Coach in agilen Projekten
  • DevOps
Datenschutz Impressum