Geb for testing your Grails Application

Jacob Aae Mikkelsen

Agenda

  • Functional testing

  • Geb - cudos and history

  • How Geb works

  • Geb and Grails 2 and 3

  • Browser support

  • Javascript

Jacob Aae Mikkelsen

  • Senior Engineer at Lego

    • Microservice based architechture on JVM

  • Previously 4 years at Gennemtænkt IT

    • Consultant on Groovy and Grails

  • External Associate Professor - University of Southern Denmark

  • Twitter @JacobAae

  • Blogs The Grails Diary

Functional testing

  • Ignores the specifics of the underlying software component under test.

  • Merely asserts that providing certain input results in certain output.

  • Web-application: Programmatically controlling a web browser to simulate the actions of a user on a web page.

Traditionally been tedious, cumbersome and brittle to do
Geb helps ease the pain

Geb

Credits

  • Luke Daley

  • Marcin Erdmann

Geb history

  • Started in November 2009

  • Created by Luke Daley

  • Current project lead Marcin Erdman

Why Geb

  • jQuery like selector syntax

  • Power of WebDriver (Easier api)

  • Robustness of Page Object modeling

  • Expressiveness of the Groovy language

  • Integrates well with build systems (Gradle/Maven)

  • Excellent user manual/documentation

Geb Implementation

  • Build on top of the WebDriver browser automation library

    • successor to the Selenium Remote Control (RC) testing framework.

    • Selenium RC → JavaScript to interact

    • WebDriver → native browser drivers

  • Use JUnit or Spock

Using Geb

Geb Selectors (1)

Jquery like syntax

// match all 'p' elements on page
$("p")

// match the first 'p' element on the page
$("p", 0)

// All 'p' elements with a title value 'section'
$("div", title: "section")

// match the first 'p' element title attribute 'section'
$("p", 0, title: "section")

// match the first 'p' element with the class 'main'
$("p.main", 0)

Geb Selectors (2)

Selecting returns Navigator objects

// The parent of the first div
$("div", 0).parent()

// All tables with a cellspacing
// attribute value of 0 that are nested in a paragraph
$("p").find("table", cellspacing: '0')

Retriving Information

$("p").text() == "a"
$("p").tag() == "p"
$("p").@title == "a"
$("p").classes() == ["a", "para"]

Interaction with content

  • click()

  • isDisplayed()

  • withConfirm{}

  • withAlert{}

$("a.btn").click()
$("div").isDisplayed()

withConfirm {
    $("button.delete").click()
}

Sending input

import org.openqa.selenium.Keys

// Shorthand for sendKeys() method of WebDriver.
$("div") << "abc"

$("input", name: "foo") << Keys.chord(Keys.CONTROL, "c")

More interaction with forms

Consider the following HTML…

<form>
    <input type="text" name="geb" value="Functional" />
</form>

The value can be read and written via property notation…

$("form").geb == "Functional"
$("form").geb = "Testing"
$("form").geb == "Testing"

These are literally shortcuts for…

$("form").find("input", name: "geb").value() == "Functional"
$("form").find("input", name: "geb").value("Testing")
$("form").find("input", name: "geb").value() == "Testing"

Variables Available

  • title

  • browser

  • currentUrl

  • currentWindow

More possibilities

  • Uploading files

  • Interaction closures

    • Simulate drag-n-drop

    • Control-clicking

  • Interacting with javascript

    • js object

Structuring Geb Tests

Lets test a CRUD part of a grails application registrering conference attendees

Lets test the following

  1. Goto list of attendees page

  2. Create new employee (incl. invalid data once)

  3. Update the employee

  4. Check data is updated

Geb Spec Basics

import geb.spock.GebSpec

@Stepwise // Ensures the tests are run sequentially
class AttendeeFunctionalSpec extends GebSpec {

    // Spock specs here
}

Geb Spec (1)

The naive inmaintainable way!

void "Go to list page - check initial state"() {
    when:"The home page is visited"
    go '/attendee/index'

    then:
    title == "Attendee List"
}

Geb Spec (2)

The naive inmaintainable way!

void "Click new attendee button"() {
    when:
    $("a.create").click()

    then:
    title == "Create Attendee"
}

Geb Spec (3)

The naive inmaintainable way!

void "Submit form with errors"() {
    when:
    $("button.btn-primary").click()

    then:
    title == "Create Attendee"
}

Geb Spec (4)

The naive inmaintainable way!

void "Submit form with no errors"() {
    when:
    $('form').name = 'Bobby'
    $('form').email = 'bobby@mail.dk'

    and:
    $("button.btn-primary").click()

    then:
    title == 'Show Attendee'
    $('span.property-value').find{ it.text() == 'Bobby'}
    $('span.property-value').find{ it.text() == 'bobby@mail.dk'}
}

Geb Spec (5)

The naive inmaintainable way!

void "Click Edit Button"() {
    when:
    $("a.btn-primary").click()

    then:
    title == 'Edit Attendee'
}

Geb Spec (6)

The naive inmaintainable way!

void "Update Attendee"() {
    when:
    $('form').name = 'Alison'
    $('form').email = 'alison@gr8.dk'

    and:
    $("button.btn-primary").click()

    then:
    title == 'Show Attendee'
    $('span.property-value').find{ it.text() == 'Alison'}
    $('span.property-value').find{ it.text() == 'alison@gr8.dk'}
}

Geb Spec - The Better Way

If we make a few scenarios, there will be

  • Much duplication

  • Many places to correct if we change the layout / DOM

We can correct this using pages and modules

Page Objects

Describes a web page

  • Url

  • How to check if we are at the correct place

  • Content we wish to interact with

Page Objects

import eu.gr8conf.grailsdemo.modules.NavigationBarModule
import geb.Page

class AttendeeShowPage extends Page {

    static url = "/attendee/show"

    static at = { title ==~ /Show Attendee/ }

    static content = {
        attProp{ $('span.property-label') }
        name{ attProp.find{ it.text() == 'Name'}.next().text() }
        email{ attPro.find{ it.text() == 'Email'}.next().text() }
        editButton{ $("a.btn-primary") }
    }
}

Modules

Describes repeated content

  • Across pages

  • Within the same page

Modules

import geb.Module

class NavigationBarModule extends Module {

    static base = { $('nav.navbar') }

    static content = {
        home(required: false) { $('a.home') }
        listAttendee(required: false) { $('a.list') }
        newAttendee(required: false) { $('a.create') }
    }
}

Modules

static content = {
// Like this, the module does not need a base
//  form{ module NavigationBarModule, $('nav.navbar') }
    form { module NavigationBarModule }
}

Module for repeated content in a page

import geb.Module

class AttendeeListItemModule extends Module {

    static content = {
        data { $("td", it) }
        name { data(0).text() }
        email { data(1).text() }
        nationality { data(2).text() }
        dateCreated { data(3).text() }
        lastUpdated { data(4).text() }
    }
}

Module for repeated content in a page

AttendeeListPage.groovy
static content = {
  menubar { module NavigationBarModule }
  attendees { moduleList AttendeeListItemModule,
                $("table tr").tail() }
}

Module for repeated content in a page

when:
to AttendeeListPage

then:
attendees*.name.contains('Guillaume Laforge')

Geb Spec - structured (1)

Lets try to restructure the ugly spec from before

void "Go to list page - check initial state"() {
    when:
    to AttendeeIndexPage

    then:
    at AttendeeIndexPage
}

Geb Spec - structured (2)

void "Click new attendee button"() {
    when:
    menubar.newAttendee.click()

    then:
    at AttendeeCreatePage
}

Geb Spec - structured (3)

void "Submit form with errors"() {
    when:
    submitButton.click()

    then:
    at AttendeeCreatePage
}

Geb Spec - structured (4)

void "Submit form with no errors"() {
    when:
    form.name = 'Bob'
    form.email = 'bob@somemail.com'

    and:
    submitButton.click()

    then:
    at AttendeeShowPage
    name == 'Bob'
    email == 'bob@somemail.com'
}

Geb Spec - structured (5)

void "Click Edit Button"() {
    when:
    editButton.click()

    then:
    at AttendeeEditPage
}

Geb Spec - structured (6)

void "Update Attendee"() {
    when:
    form.name = 'Alice'
    form.email = 'alice@somemail.com'

    and:
    updateButton.click()

    then:
    at AttendeeShowPage
    title == 'Show Attendee'
    name == 'Alice'
    email == 'alice@somemail.com'
}

Geb with Grails

Geb and Grails 2.x

Must install plugin in BuildConfig.groovy

dependencies {
  ...
  test("org.seleniumhq.selenium:selenium-support:2.45.0")
  test("org.seleniumhq.selenium:selenium-firefox-driver:2.45.0")
  test "org.gebish:geb-spock:0.10.0"
}
plugins {
  ...
  test "org.grails.plugins:geb:0.10.0"
}

Geb Tests in Grails 2.x

Tests placed in test/functional folder

Running the tests

grails test-app functional:

Geb and Grails 3

Geb is default in build.gradle

dependencies {
  ...
  testCompile "org.grails.plugins:geb"

  // Note: It is recommended to update to a more robust driver
  // (Chrome, Firefox etc.)
  testRuntime 'org.seleniumhq.selenium:selenium-htmlunit-driver:2.44.0'
}

Geb Tests in Grails 3

Creating Geb Spec

grails create-functional-test MyGebScenario

Placing the test in src/integration-test/groovy

Running the tests

grails test-app -integration

Generated Class

@Integration
@Rollback
class ManyAttendeesSpec extends GebSpec {

    void "test something"() {
        when:"The home page is visited"
        go '/'

        then:"The title is correct"
        $('title').text() == "Welcome to Grails"
    }
}

Interacting with Application

When some functionality is needed that is not exposed through the browser, it can be necessary to interact with the application under test.

Grails 2.5

Tests not running in same JVM

Done with remote-control plugin

Send a closure for execution in application

compile ":remote-control:2.0"

Remote Control

setup: 'Create some item not available through GUI'
def id = remote {
    Item item = new Item(name: "MyItem")
    item.save()
    item.id
}

Grails 3.0

Application is in same jvm

Interaction is possible directly

Tests run more like integration tests

Interacting with Application

void "Test Pagination is shown with 15 attendees"() {
    setup:
    Attendee.withNewTransaction {
       15.times {
          new Attendee(name: "N$it", email: "m$it@t.dk").save()
       }
    }

    when:
    to AttendeeIndexPage

    then:
    hasPagination()
}

Interacting with Application

static content = {
    menubar { module NavigationBarModule }
    pagination(required: false) { $('span.currentStep') }
}

boolean hasPagination() {
    pagination.text()
}

Configuration and Browser support

GebConfig

Configuration for Geb is placed in GebConfig.groovy

In Grails 3, place it in ´src/integration-test/groovy`

Driver

It is possible to configure the browser used.

build.gradle
compile 'org.seleniumhq.selenium:selenium-chrome-driver:2.45.0'
compile 'org.seleniumhq.selenium:selenium-firefox-driver:2.45.0'

Firefox

GebConfig.groovy
import org.openqa.selenium.firefox.FirefoxProfile
import org.openqa.selenium.firefox.FirefoxDriver

driver = {
    FirefoxProfile profile = new FirefoxProfile()
    profile.setPreference("browser.download.folderList", 2)
    profile.setPreference("browser.download.dir", "/tmp")
    profile.setPreference(
         "browser.helperApps.neverAsk.saveToDisk", "text/csv")

    def driverInstance = new FirefoxDriver(profile)
    driverInstance.manage().window().maximize()
    driverInstance
}

Chrome

  • Needs ChromeDriver downloaded

  • Pretty fast and stable

Chrome (1)

GebConfig.groovy
private String driverLocationDependingOnOperatingSystem() {
    String os = System.getProperty("os.name").toLowerCase();
    def loc = "http://chromedriver.storage.googleapis.com/2.15"
    if( os.contains('mac')) {
        return "${loc}/chromedriver_mac32.zip"
    }
    if( os.contains('win')) {
        return "${loc}/chromedriver_win32.zip"
    }
    return "${loc}/chromedriver_linux64.zip"
}

Chrome (2)

GebConfig.groovy
private void downloadDriver(File file, String path) {
    if (!file.exists()) {
        def ant = new AntBuilder()
        ant.get(src: path, dest: 'driver.zip')
        ant.unzip(src: 'driver.zip', dest: file.parent)
        ant.delete(file: 'driver.zip')
        ant.chmod(file: file, perm: '700')
    }
}

Chrome (3)

GebConfig.groovy
def chromeDriver = new File('build/drivers/chrome/chromedriver')
downloadDriver(chromeDriver,
        driverLocationDependingOnOperatingSystem())
System.setProperty('webdriver.chrome.driver',
        chromeDriver.absolutePath)

driver = {
    def driverInstance = new ChromeDriver()

    def browserWindow = driverInstance.manage().window()
    // width, height
    browserWindow.size = new Dimension(1000, 2500)
    browserWindow.position = new Point(0, 0)

    driverInstance
}

Waiting

GebConfig.groovy
waiting {
    timeout = 10
    retryInterval = 0.5
}

baseNavigatorWaiting = true
atCheckWaiting = true

Using Waiting

<div class="fade-me-in" style="display: none">
  Hi - are yo waiting for me?
</div>
<script>
  $('div.fade-me-in').delay(3000).slideDown();
</script>
static content = {
    fadeInMessage{ $('div.fade-me-in') }
}
then:
waitFor {
    fadeInMessage.text() == 'Hi - are yo waiting for me?'
}

Reporting

Test reports

  • Nicely formatted

  • Spock power-assert format

Screenshots

Screenshots and HTML from end of each test:

Extend from GebReportingSpec

class AttendeeFunctionalSpec extends GebReportingSpec
GebConfig.groovy
reportsDir = new File("build/geb-reports")

Ad-hoc Screenshots

report "When-form-is-just-filled"
  • Saves a report in reportsDir

  • Numbered in increasing order

Javascript

In case you need to interact using javascript

Executing Javascript

Clicking a button that is hidden will create a ElementNotVisibleException

<fieldset class="well" style="display: none">
    <g:link class="btn" action="index">List</g:link>
</fieldset>

Executing Javascript

JavascriptExecutor executor = (JavascriptExecutor) driver
executor.executeScript('jQuery(".well").show();')

Wrapping Javascript

def js( String script ){
    (driver as JavascriptExecutor).executeScript( script )
}
js('jQuery(".well").show();')

Other usages

  • Screenscraping of a site

  • Solving complex problems like 2048

Resources

Questions?