End-to-end
testing
A “spocky” story
Blatant self-promotion
Jesús L. Domínguez Muriel
@jdmuriel
Freelance, working at DIGIBÍS
End to end testing
- Chapter I: Why
- Chapter II: A spooky story
- Chapter III: Doing it with geb
Chapter I- Why: Sometimes, software starts small...
... and then it grows
Then, any small change can awake the beast
Ways to handle a 15 year software beast
- Flee
- Rewrite
- Refactor
How to handle a 15 year software beast (I): Flee
(No quiero mirar a nadie :-P )
How to handle a 15 year software beast (II): Rewrite
(Usually not an option)
How to handle a 15 year software beast (III): Refactor
- Refactor need tests
- You usually have untestable code
- Start testing at the lowest level possible
- Unit
- Integration
- End-to-end
- Add new code with good testing practices
- References
- https://www.youtube.com/watch?v=6W-EyIfUTmI
End-to-end tests
- Aka functional tests
- Test the application as an end user would do it
- Backend + frontend + everything in between
- Good for
- Acceptance criteria in user stories
- Systems with many coordinated pieces
- Testing untestable code
End to end testing
wisdom (I)
“End to end tests are like
cats”
(they always do the
same until they don’t)
End to end testing
wisdom (II)
““As anyone who has tried
knows, maintaining a large
suite of functional web tests for
a changing application can
become an expensive and
frustrating process””
Chapter II: a spooky story
Our story starts
with a library
With the years, it somehow has grown in other thing
Digibib - later Digiarch, Digihub, Digimus
- 2003 - 2017
- 43 jars, 136 dependencies
- Several hundred thousands lines of code
- Our own framework -in 2003 there is not Spring
- XML based :-(
- Reimplementations
- Code who nobody knows if it is used or not
- Several generations of front-ends
- Some time around 2012 we start thinking we need some tests
First iteration: Selenium IDE for non technical users
- Firefox extension
- Records browser activity
- Generates an script
- Small IDE to edit the script
- Script can be replayed
- Can be automated with some
effort
- Usable by trained non
technical users…?
- Firefox extension
- Records browser activity
- Generates an script
- Small IDE to edit the script
- Script can be replayed
- Can be automated with some
effort
- Usable by trained non
technical users…?
First iteration: Selenium IDE for non technical users
Selenium IDE: not what we expected
- Selecting the element to check not easy for non technical users (even
if the tool allows selecting it in the browser)
- By CSS
- By XPath
- Changes in a page require recapture of whole, long scripts
- Firefox updates break the extension
- Automation too brittle
- Multi script test execution too slow
- We never get end users to really use the IDE
Selenium IDE: not what we expected
Second iteration: exporting scripts to Java
Advantages
- News scripts can be created faster
- Easier to integrate with jenkins
- Easier to use other browsers
- Improved speed using headless browser
PhantomJS
Second iteration: exporting scripts to Java
Disadvantages
“The key to not pulling your hair out when dealing with web tests” - @ldaley
Tests call domain methods, no HTML
becomes
Access to the specific HTML and CSS only within that Page Objects
If the HTML is changed, only the affected Page object must be changed
Even Martin Fowler said it!!! (https://martinfowler.com/bliki/PageObject.html)
Page object pattern
So, we have work to do
3 or 4 most tested pages are already converted to Page objects
Login
Search form
Many still to do:
Results
Configuration, Indexes, etc.
Chapter III: Enters geb
Developer focused tool
Uses Groovy's dynamism to remove boilerplate, achieve pseudo English code
Uses WebDriver (evolution of Selenium 2) - Cross browser
Inspired by jQuery, robust Page Object modelling support
Good documentation. The “Book of Geb” http://www.gebish.org/manual/current/
Luke Daley (Gradle, Ratpack), Marcin Erdmann (current)
Why geb
Concise
Team already using Groovy with Spock for tests
Standard, tested implementation of utilities we were implementing ad hoc:
- Driver configuration
- Timeout configuration
- Screenshots
- Integration
You can give a talk!!!
Why not geb
Dependency madness (specially with Maven and eclipse)
No autocomplete (on Eclipse)
Oriented to Groovy developers
Geb in 10 slides - Browser
Browser.drive {
go "http://gebish.org"
assert title == "Geb - Very Groovy Browser Automation"
$("div.menu a.manuals").click()
waitFor { !$("#manuals-menu").hasClass("animating") }
$("#manuals-menu a")[0].click()
assert title.startsWith("The Book Of Geb")
}
Browser always have a current page and delegates methods and properties to it.
Geb in 10 slides - Test adapter
class GebishOrgTest extends GebSpec {
@Test
void “clicking first manual goes to book of geb”() {
given:
go "http://gebish.org" //Delegate to browser
when:
$("div.menu a.manuals").click() //Delegate to page
waitFor { !$("#manuals-menu").hasClass("animating") }
$("#manuals-menu a")[0].click()
then:
title.startsWith("The Book Of Geb")
}
}
//Automatic screenshot reporting after every test
Geb in 10 slides - $(), Navigator
Inspired by jQuery:
//$("<css selector>", index_or_range, <attribute/text matcher>)
$("div>p", 0..2, class: "heading", text: iEndsWith("!")) == [“a!”, “b!”]
$("p", text: ~/This./).size() == 2
$(By.id("some-id"))
$(By.xpath('//p[@class="xpath"]'))
//Navigators are iterable
$("p").max { it.text() }.text() == "2"
$("p")*.text().max() == "2"
//Methods for filtering, element traversal
$("div").filter(".a").not(".c").has("p", text: "Found!").hasNot("br")
$("div").find("p").parent().next().children("a", href: contains("www"))
//nextAll(), previous(), siblings(), parents(), closest(),
//nextUntil(), prevUntil(), parentsUntil()
Geb in 10 slides - $(), Navigator
//Composition
$( $("p.a"), $("p.b") )
//Methods
$("a.login").click()
$("input") << "test" << Keys.chord(Keys.CONTROL, "c")
//Properties
displayed, focused //single element needed
height, width, x, y
text(), tag(), classes(), @attribute
//CSS Properties
css("<css property>")
Geb in 10 slides - Page objects
class GebHomePage extends Page {
static url = "http://gebish.org"
static at = { title.contains "Groovy" }
static content = {
toggle { $("div.menu a.manuals") }
linksContainer { $("#manuals-menu") }
links { linksContainer.find("a") }
//stacked content
}
}
class TheBookOfGebPage extends Page {
//url is optional
static at = {
title.startsWith("The Book Of Geb")
}
}
void “clicking first manual goes to book of geb”() {
given:
to GebHomePage //checks at
when:
toggle.click()
waitFor {
!linksContainer.hasClass("animating")
}
links[0].click()
then:
at TheBookOfGebPage
}
Geb in 10 slides - Module
class GebHomePage extends Page {
static url = "http://gebish.org"
static at = { title.contains "Groovy" }
static content = {
manualsMenu { module(ManualsMenuModule)}
}
}
class ManualsMenuModule extends Module {
static content = {
toggle { $("div.menu a.manuals") }
linksContainer { $("#manuals-menu") }
links { linksContainer.find("a") }
}
void open() {
toggle.click()
waitFor { !linksContainer.hasClass
("animating") }
}
}
void “clicking first manual goes to book of geb”() {
given:
to GebHomePage //checks at
when:
manualsMenu.open()
manualsMenu.links[0].click()
then:
at TheBookOfGebPage
}
Geb in 10 slides - Content DSL, moduleList()
static content = {
«name»(«options map») { «definition» }
theDiv(required: false) { $("div", id: "a") }
theDiv(min: 1, max: 2) { $("div", id: "a") }
theDiv(cache: false) { $("div", id: "a") }
helpLink(to: HelpPage) { $("a", text: "Help") } //helpLink.click() sets browser page
loginButton(to: [LoginSuccessfulPage, LoginFailedPage]) { $("input.loginButton") }
dynamicallyAdded(wait: true) { $("p.dynamic") }
someDiv { $("div#aliased") }
aliasedDiv(aliases: "someDiv")
firstCartItem { $("table tr", 0) module (CartRow)}
cartItems {
$("table tr").tail().moduleList(CartRow)
}
assert cartItems.every { it.price > 0.0 }
Geb in 10 slides - Interact API, javascript interface
interact {
clickAndHold($('#draggable'))
moveByOffset(150, 200)
release()
} //All WebDriver Actions
<html>
<head>
<script type="text/javascript">
var aVariable = 1;
</script>
</head>
</html>
assert Browser.js.aVariable == 1
$("div#a").jquery.mouseover()
Geb in 10 slides - Configuration
GebConfig.groovy or GebConfig class in classpath
driver = “firefox” //driver = { new FirefoxDriver() }
environments {
prod {
driver = chrome
}
}
waiting {
timeout = 10
retryInterval = 0.5
presets {
slow {
Timeout = 20
}
}
}
retorsDir = “target/geb-reports”
Geb in 10 slides - Download API
Browser.drive {
to LoginPage
login("me", "secret")
def pdfBytes = downloadBytes(pdfLink.@href)
}
//downloadStream, downloadText, downloadContent
Browser.drive {
go "/"
def jsonBytes = downloadBytes { HttpURLConnection connection ->
connection.setRequestProperty("Accept", "application/json")
}
}
Geb in 10 slides - windows & frames
<a href="http://www.gebish.org" target="myWindow">Geb</a>
Browser.drive {
go()
$("a").click()
withWindow("myWindow", close: true) {
assert title == "Geb - Very Groovy Browser Automation"
}
withWindow({ title == "Geb - Very Groovy Browser Automation" }) {
assert $(".slogan").text().startsWith("Very Groovy browser automation.")
}
}
//withNewWindow(), withFrame(), withNewFrame()
Closures everywhere!
Contents, modules,
waitFor, browser, interact
Rewritten AST “a la Spock”, so that every
expression is turned in an assert (in ats, waits)
Wrapped classes - all page, browser available in
test specs
MethodMissing,
PropertyMissing
Geb’s black magic
Summary
If you have a big application,
use end-to-end tests
If you use Groovy, use Geb
Don’t walk alone in a misty forest...
Thanks
Questions?
@jdmuriel

End-to-end testing with geb

  • 1.
  • 2.
    Blatant self-promotion Jesús L.Domínguez Muriel @jdmuriel Freelance, working at DIGIBÍS
  • 3.
    End to endtesting - Chapter I: Why - Chapter II: A spooky story - Chapter III: Doing it with geb
  • 4.
    Chapter I- Why:Sometimes, software starts small...
  • 5.
    ... and thenit grows
  • 6.
    Then, any smallchange can awake the beast
  • 7.
    Ways to handlea 15 year software beast - Flee - Rewrite - Refactor
  • 8.
    How to handlea 15 year software beast (I): Flee (No quiero mirar a nadie :-P )
  • 9.
    How to handlea 15 year software beast (II): Rewrite (Usually not an option)
  • 10.
    How to handlea 15 year software beast (III): Refactor - Refactor need tests - You usually have untestable code - Start testing at the lowest level possible - Unit - Integration - End-to-end - Add new code with good testing practices - References - https://www.youtube.com/watch?v=6W-EyIfUTmI
  • 11.
    End-to-end tests - Akafunctional tests - Test the application as an end user would do it - Backend + frontend + everything in between - Good for - Acceptance criteria in user stories - Systems with many coordinated pieces - Testing untestable code
  • 12.
    End to endtesting wisdom (I) “End to end tests are like cats” (they always do the same until they don’t)
  • 13.
    End to endtesting wisdom (II) ““As anyone who has tried knows, maintaining a large suite of functional web tests for a changing application can become an expensive and frustrating process””
  • 14.
    Chapter II: aspooky story Our story starts with a library
  • 15.
    With the years,it somehow has grown in other thing
  • 16.
    Digibib - laterDigiarch, Digihub, Digimus - 2003 - 2017 - 43 jars, 136 dependencies - Several hundred thousands lines of code - Our own framework -in 2003 there is not Spring - XML based :-( - Reimplementations - Code who nobody knows if it is used or not - Several generations of front-ends - Some time around 2012 we start thinking we need some tests
  • 17.
    First iteration: SeleniumIDE for non technical users - Firefox extension - Records browser activity - Generates an script - Small IDE to edit the script - Script can be replayed - Can be automated with some effort - Usable by trained non technical users…?
  • 18.
    - Firefox extension -Records browser activity - Generates an script - Small IDE to edit the script - Script can be replayed - Can be automated with some effort - Usable by trained non technical users…? First iteration: Selenium IDE for non technical users
  • 19.
    Selenium IDE: notwhat we expected
  • 20.
    - Selecting theelement to check not easy for non technical users (even if the tool allows selecting it in the browser) - By CSS - By XPath - Changes in a page require recapture of whole, long scripts - Firefox updates break the extension - Automation too brittle - Multi script test execution too slow - We never get end users to really use the IDE Selenium IDE: not what we expected
  • 21.
    Second iteration: exportingscripts to Java Advantages - News scripts can be created faster - Easier to integrate with jenkins - Easier to use other browsers - Improved speed using headless browser PhantomJS
  • 22.
    Second iteration: exportingscripts to Java Disadvantages
  • 23.
    “The key tonot pulling your hair out when dealing with web tests” - @ldaley Tests call domain methods, no HTML becomes Access to the specific HTML and CSS only within that Page Objects If the HTML is changed, only the affected Page object must be changed Even Martin Fowler said it!!! (https://martinfowler.com/bliki/PageObject.html) Page object pattern
  • 24.
    So, we havework to do 3 or 4 most tested pages are already converted to Page objects Login Search form Many still to do: Results Configuration, Indexes, etc.
  • 25.
    Chapter III: Entersgeb Developer focused tool Uses Groovy's dynamism to remove boilerplate, achieve pseudo English code Uses WebDriver (evolution of Selenium 2) - Cross browser Inspired by jQuery, robust Page Object modelling support Good documentation. The “Book of Geb” http://www.gebish.org/manual/current/ Luke Daley (Gradle, Ratpack), Marcin Erdmann (current)
  • 26.
    Why geb Concise Team alreadyusing Groovy with Spock for tests Standard, tested implementation of utilities we were implementing ad hoc: - Driver configuration - Timeout configuration - Screenshots - Integration You can give a talk!!!
  • 27.
    Why not geb Dependencymadness (specially with Maven and eclipse) No autocomplete (on Eclipse) Oriented to Groovy developers
  • 28.
    Geb in 10slides - Browser Browser.drive { go "http://gebish.org" assert title == "Geb - Very Groovy Browser Automation" $("div.menu a.manuals").click() waitFor { !$("#manuals-menu").hasClass("animating") } $("#manuals-menu a")[0].click() assert title.startsWith("The Book Of Geb") } Browser always have a current page and delegates methods and properties to it.
  • 29.
    Geb in 10slides - Test adapter class GebishOrgTest extends GebSpec { @Test void “clicking first manual goes to book of geb”() { given: go "http://gebish.org" //Delegate to browser when: $("div.menu a.manuals").click() //Delegate to page waitFor { !$("#manuals-menu").hasClass("animating") } $("#manuals-menu a")[0].click() then: title.startsWith("The Book Of Geb") } } //Automatic screenshot reporting after every test
  • 30.
    Geb in 10slides - $(), Navigator Inspired by jQuery: //$("<css selector>", index_or_range, <attribute/text matcher>) $("div>p", 0..2, class: "heading", text: iEndsWith("!")) == [“a!”, “b!”] $("p", text: ~/This./).size() == 2 $(By.id("some-id")) $(By.xpath('//p[@class="xpath"]')) //Navigators are iterable $("p").max { it.text() }.text() == "2" $("p")*.text().max() == "2" //Methods for filtering, element traversal $("div").filter(".a").not(".c").has("p", text: "Found!").hasNot("br") $("div").find("p").parent().next().children("a", href: contains("www")) //nextAll(), previous(), siblings(), parents(), closest(), //nextUntil(), prevUntil(), parentsUntil()
  • 31.
    Geb in 10slides - $(), Navigator //Composition $( $("p.a"), $("p.b") ) //Methods $("a.login").click() $("input") << "test" << Keys.chord(Keys.CONTROL, "c") //Properties displayed, focused //single element needed height, width, x, y text(), tag(), classes(), @attribute //CSS Properties css("<css property>")
  • 32.
    Geb in 10slides - Page objects class GebHomePage extends Page { static url = "http://gebish.org" static at = { title.contains "Groovy" } static content = { toggle { $("div.menu a.manuals") } linksContainer { $("#manuals-menu") } links { linksContainer.find("a") } //stacked content } } class TheBookOfGebPage extends Page { //url is optional static at = { title.startsWith("The Book Of Geb") } } void “clicking first manual goes to book of geb”() { given: to GebHomePage //checks at when: toggle.click() waitFor { !linksContainer.hasClass("animating") } links[0].click() then: at TheBookOfGebPage }
  • 33.
    Geb in 10slides - Module class GebHomePage extends Page { static url = "http://gebish.org" static at = { title.contains "Groovy" } static content = { manualsMenu { module(ManualsMenuModule)} } } class ManualsMenuModule extends Module { static content = { toggle { $("div.menu a.manuals") } linksContainer { $("#manuals-menu") } links { linksContainer.find("a") } } void open() { toggle.click() waitFor { !linksContainer.hasClass ("animating") } } } void “clicking first manual goes to book of geb”() { given: to GebHomePage //checks at when: manualsMenu.open() manualsMenu.links[0].click() then: at TheBookOfGebPage }
  • 34.
    Geb in 10slides - Content DSL, moduleList() static content = { «name»(«options map») { «definition» } theDiv(required: false) { $("div", id: "a") } theDiv(min: 1, max: 2) { $("div", id: "a") } theDiv(cache: false) { $("div", id: "a") } helpLink(to: HelpPage) { $("a", text: "Help") } //helpLink.click() sets browser page loginButton(to: [LoginSuccessfulPage, LoginFailedPage]) { $("input.loginButton") } dynamicallyAdded(wait: true) { $("p.dynamic") } someDiv { $("div#aliased") } aliasedDiv(aliases: "someDiv") firstCartItem { $("table tr", 0) module (CartRow)} cartItems { $("table tr").tail().moduleList(CartRow) } assert cartItems.every { it.price > 0.0 }
  • 35.
    Geb in 10slides - Interact API, javascript interface interact { clickAndHold($('#draggable')) moveByOffset(150, 200) release() } //All WebDriver Actions <html> <head> <script type="text/javascript"> var aVariable = 1; </script> </head> </html> assert Browser.js.aVariable == 1 $("div#a").jquery.mouseover()
  • 36.
    Geb in 10slides - Configuration GebConfig.groovy or GebConfig class in classpath driver = “firefox” //driver = { new FirefoxDriver() } environments { prod { driver = chrome } } waiting { timeout = 10 retryInterval = 0.5 presets { slow { Timeout = 20 } } } retorsDir = “target/geb-reports”
  • 37.
    Geb in 10slides - Download API Browser.drive { to LoginPage login("me", "secret") def pdfBytes = downloadBytes(pdfLink.@href) } //downloadStream, downloadText, downloadContent Browser.drive { go "/" def jsonBytes = downloadBytes { HttpURLConnection connection -> connection.setRequestProperty("Accept", "application/json") } }
  • 38.
    Geb in 10slides - windows & frames <a href="http://www.gebish.org" target="myWindow">Geb</a> Browser.drive { go() $("a").click() withWindow("myWindow", close: true) { assert title == "Geb - Very Groovy Browser Automation" } withWindow({ title == "Geb - Very Groovy Browser Automation" }) { assert $(".slogan").text().startsWith("Very Groovy browser automation.") } } //withNewWindow(), withFrame(), withNewFrame()
  • 39.
    Closures everywhere! Contents, modules, waitFor,browser, interact Rewritten AST “a la Spock”, so that every expression is turned in an assert (in ats, waits) Wrapped classes - all page, browser available in test specs MethodMissing, PropertyMissing Geb’s black magic
  • 40.
    Summary If you havea big application, use end-to-end tests If you use Groovy, use Geb Don’t walk alone in a misty forest...
  • 41.