geb for testing your grails application
TRANSCRIPT
GEB FOR TESTING YOUR GRAILSAPPLICATION
Jacob Aae Mikkelsen
AGENDAFunctional testing
Geb - cudos and history
How Geb works
Geb and Grails 2 and 3
Browser support
Javascript
JACOB AAE MIKKELSENSenior 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 SouthernDenmark
@JacobAae
Blogs The Grails Diary
FUNCTIONAL TESTINGIgnores the specifics of the underlying softwarecomponent under test.
Merely asserts that providing certain input results incertain output.
Web-application: Programmatically controlling a webbrowser to simulate the actions of a user on a web page.
Traditionally been tedious, cumbersome and brittleto do
Geb helps ease the pain
GEB
CREDITSLuke Daley
Marcin Erdmann
GEB HISTORYStarted in November 2009
Created by Luke Daley
Current project lead Marcin Erdman
WHY GEBjQuery 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 IMPLEMENTATIONBuild on top of the WebDriver browser automation library
successor to the Selenium Remote Control (RC) testingframework.
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 CONTENTclick()
isDisplayed()
withConfirm
withAlert
$("a.btn").click()$("div").isDisplayed()
withConfirm $("button.delete").click()
SENDING INPUTimport org.openqa.selenium.Keys
// Shorthand for sendKeys() method of WebDriver.$("div") << "abc"
$("input", name: "foo") << Keys.chord(Keys.CONTROL, "c")
MORE INTERACTION WITH FORMSConsider 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 AVAILABLEtitle
browser
currentUrl
currentWindow
MORE POSSIBILITIESUploading files
Interaction closures
Simulate drag-n-drop
Control-clicking
Interacting with javascript
js object
STRUCTURING GEB TESTSLets 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 BASICSimport geb.spock.GebSpec
@Stepwise // Ensures the tests are run sequentiallyclass 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.btnprimary").click()
then: title == "Create Attendee"
GEB SPEC (4)The naive inmaintainable way!
void "Submit form with no errors"() when: $('form').name = 'Bobby' $('form').email = '[email protected]'
and: $("button.btnprimary").click()
then: title == 'Show Attendee' $('span.propertyvalue').find it.text() == 'Bobby' $('span.propertyvalue').find it.text() == '[email protected]'
GEB SPEC (5)The naive inmaintainable way!
void "Click Edit Button"() when: $("a.btnprimary").click()
then: title == 'Edit Attendee'
GEB SPEC (6)The naive inmaintainable way!
void "Update Attendee"() when: $('form').name = 'Alison' $('form').email = '[email protected]'
and: $("button.btnprimary").click()
then: title == 'Show Attendee' $('span.propertyvalue').find it.text() == 'Alison' $('span.propertyvalue').find it.text() == '[email protected]'
GEB SPEC - THE BETTER WAYIf 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 OBJECTSDescribes a web page
Url
How to check if we are at the correct place
Content we wish to interact with
PAGE OBJECTSimport eu.gr8conf.grailsdemo.modules.NavigationBarModuleimport geb.Page
class AttendeeShowPage extends Page
static url = "/attendee/show"
static at = title ==~ /Show Attendee/
static content = attProp $('span.propertylabel') name attProp.find it.text() == 'Name'.next().text() email attPro.find it.text() == 'Email'.next().text() editButton $("a.btnprimary")
MODULESDescribes repeated content
Across pages
Within the same page
MODULESimport 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')
MODULESstatic content = // Like this, the module does not need a base// form module NavigationBarModule, $('nav.navbar') form module NavigationBarModule
MODULE FOR REPEATED CONTENTIN 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 CONTENTIN A PAGE
AttendeeListPage.groovy
static content = menubar module NavigationBarModule attendees moduleList AttendeeListItemModule, $("table tr").tail()
MODULE FOR REPEATED CONTENTIN 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 = '[email protected]'
and: submitButton.click()
then: at AttendeeShowPage name == 'Bob' email == '[email protected]'
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 = '[email protected]'
and: updateButton.click()
then: at AttendeeShowPage title == 'Show Attendee' name == 'Alice' email == '[email protected]'
GEB WITH GRAILS
GEB AND GRAILS 2.XMust install plugin in BuildConfig.groovy
dependencies ... test("org.seleniumhq.selenium:seleniumsupport:2.45.0") test("org.seleniumhq.selenium:seleniumfirefoxdriver:2.45.0") test "org.gebish:gebspock:0.10.0"plugins ... test "org.grails.plugins:geb:0.10.0"
GEB TESTS IN GRAILS 2.XTests placed in test/functional folder
Running the tests
grails testapp functional:
GEB AND GRAILS 3Geb 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:seleniumhtmlunitdriver:2.44.0
GEB TESTS IN GRAILS 3Creating Geb Spec
grails createfunctionaltest MyGebScenario
Placing the test in src/integration-test/groovy
Running the tests
grails testapp integration
GENERATED CLASS@Integration@Rollbackclass 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 APPLICATIONWhen some functionality is needed that is not exposed
through the browser, it can be necessary to interact with theapplication under test.
GRAILS 2.5Tests not running in same JVM
Done with remote-control plugin
Send a closure for execution in application
compile ":remotecontrol:2.0"
REMOTE CONTROLsetup: 'Create some item not available through GUI'def id = remote Item item = new Item(name: "MyItem") item.save() item.id
GRAILS 3.0Application is in same jvm
Interaction is possible directly
Tests run more like integration tests
INTERACTING WITH APPLICATIONvoid "Test Pagination is shown with 15 attendees"() setup: Attendee.withNewTransaction 15.times new Attendee(name: "N$it", email: "[email protected]").save()
when: to AttendeeIndexPage
then: hasPagination()
INTERACTING WITH APPLICATIONstatic content = menubar module NavigationBarModule pagination(required: false) $('span.currentStep')
boolean hasPagination() pagination.text()
CONFIGURATION AND BROWSERSUPPORT
GEBCONFIGConfiguration for Geb is placed in GebConfig.groovy
In Grails 3, place it in ´src/integration-test/groovy`
http://www.gebish.org/manual/current/configuration.html
DRIVERIt is possible to configure the browser used.
build.gradle
compile 'org.seleniumhq.selenium:seleniumchromedriver:2.45.0'compile 'org.seleniumhq.selenium:seleniumfirefoxdriver:2.45.0'
FIREFOXGebConfig.groovy
import org.openqa.selenium.firefox.FirefoxProfileimport 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
CHROMENeeds 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
WAITINGGebConfig.groovy
waiting timeout = 10 retryInterval = 0.5
baseNavigatorWaiting = trueatCheckWaiting = true
USING WAITING<div class="fademein" style="display: none"> Hi are yo waiting for me?</div><script> $('div.fademein').delay(3000).slideDown();</script>
static content = fadeInMessage $('div.fademein')
then:waitFor fadeInMessage.text() == 'Hi are yo waiting for me?'
REPORTING
TEST REPORTSNicely formatted
Spock power-assert format
SCREENSHOTSScreenshots and HTML from end of each test:
Extend from GebReportingSpec
class AttendeeFunctionalSpec extends GebReportingSpec
GebConfig.groovy
reportsDir = new File("build/gebreports")
AD-HOC SCREENSHOTSreport "Whenformisjustfilled"
Saves a report in reportsDir
Numbered in increasing order
JAVASCRIPTIn case you need to interact using javascript
EXECUTING JAVASCRIPTClicking 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 JAVASCRIPTJavascriptExecutor executor = (JavascriptExecutor) driverexecutor.executeScript('jQuery(".well").show();')
WRAPPING JAVASCRIPTdef js( String script ) (driver as JavascriptExecutor).executeScript( script )
js('jQuery(".well").show();')
OTHER USAGESScreenscraping of a site
Solving complex problems like 2048
RESOURCEShttp://gebish.org
https://github.com/geb
https://gist.github.com/melix/9619800
https://fbflex.wordpress.com/2010/08/25/geb-and-grails-tips-tricks-and-gotchas/
https://github.com/JacobAae/eu-gr8conf-grailsdemo
QUESTIONS?