Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing Web Applications with Geb

Testing Web Applications with Geb

Talk given at CodeMash 2013 covering the Geb web browser automation and testing framework.

Howard M. Lewis Ship

January 10, 2013
Tweet

More Decks by Howard M. Lewis Ship

Other Decks in Technology

Transcript

  1. Testing Web Applications with GEB Howard M. Lewis Ship TWD

    Consulting [email protected] Twitter: @hlship © 2013 Howard M. Lewis Ship
  2. My Path 1990's Wrote own test framework – in PL/1

    – 5000 - 7000 tests 2001 First JUnit tests (Tapestry template parser) 2003-ish Started using TestNG 2004-ish Started using EasyMock 2005-ish Started using Selenium 1 2006-ish Dabbled in Groovy 2010 Spock! ~ 0.4 2011 Geb 0.5.0
  3. WebDriver driver = new FirefoxDriver(); driver.get("http://localhost:8080/order-summary/12345"); WebElement subtotal = driver.findElement(

    By.cssSelector(".order-summary .subtotal"); assertEquals(subtotal.getText(), "107.95"); WebElement submit = driver.findElement( By.cssSelector(".order-form input[type=submit]")); submit.click() Navigation
  4. WebDriver driver = new FirefoxDriver(); driver.get("http://localhost:8080/order-summary/12345"); WebElement subtotal = driver.findElement(

    By.cssSelector(".order-summary .subtotal"); assertEquals(subtotal.getText(), "107.95"); WebElement submit = driver.findElement( By.cssSelector(".order-form input[type=submit]")); submit.click() Traversal
  5. WebDriver driver = new FirefoxDriver(); driver.get("http://localhost:8080/order-summary/12345"); WebElement subtotal = driver.findElement(

    By.cssSelector(".order-summary .subtotal"); assertEquals(subtotal.getText(), "107.95"); WebElement submit = driver.findElement( By.cssSelector(".order-form input[type=submit]")); submit.click() Interaction
  6. driver.get("http://www.google.com"); WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); new WebDriverWait(driver, 10)).until(new

    ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }; Triggers Ajax Update Triggers Ajax Update
  7. ✦Power of Selenium WebDriver 2 ✦Elegance of jQuery content selection

    ✦Robustness of Page Object modelling ✦Expressiveness of Groovy ✦First Class Documentation
  8. Running GEB Interactively import groovy.grape.Grape Grape.grab([group:'org.codehaus.geb', module:'geb-core', version:'0.7.2']) Grape.grab([group:'org.seleniumhq.selenium', module:'selenium-firefox-driver',

    version:'2.27.0']) Grape.grab([group:'org.seleniumhq.selenium', module:'selenium-support', ⏎ version:'2.27.0']) import geb.* import java.util.logging.* new File("geb-logging.properties").withInputStream { ⏎ LogManager.logManager.readConfiguration it } geb.groovy handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level=WARNING java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter geb-logging.properties
  9. GEB Interactive $ groovysh Groovy Shell (1.8.6, JVM: 1.6.0_29) Type

    'help' or '\h' for help. ------------------------------------------------- groovy:000> . geb.groovy ===> [import groovy.grape.Grape] ===> null ===> null ===> null ===> [import groovy.grape.Grape, import geb.*] ===> [import groovy.grape.Grape, import geb.*, import ⏎ java.util.logging.*] ===> null groovy:000> b = new Browser(baseUrl: "http://google.com/") ===> geb.Browser@190a0d51 groovy:000> b.go() ===> null groovy:000>
  10. Browser go() : void quit() : void page : Page

    Page pageUrl : String title : String $(…) : Navigator waitFor(…) : Object startsWith(…) : TextMatcher contains(…) : TextMatcher endsWith(…) : TextMatcher Navigator x : int y : int width : int height : int disabled : boolean empty : boolean displayed : boolean add(String) : Navigator click() : void filter(...) : Navigator find(…) : Navigator not(String) : Navigator first() : Navigator last() : Navigator getAt(…) : Navigator getAttribute(String) : String has(String) : Navigator parents(String) : Navigator parentsUntil(String): Navigator size() : int text() : String value() : Object Delegation GebSpec Delegation
  11. ✦$("p") ➠ all <p> ✦$("p", 3) ➠ 4th <p> ✦$("p")[3]

    ➠ 4th <p> ✦$("p")[0..2] ➠ 1st through 3rd <p> $(css selector, index, attribute / text matchers) CSS3
  12. Attribute Matchers ✦$("a", text:"Blade Runner") ➠ All <a> tags whose

    text is "Blade Runner" ✦$("a", href: contains("/name/") ➠ All <a> tags whose href attribute contains "/name/" ✦$("a", href: ~/nm[0-9]+/) ➠ All <a> tags whose href attribute matches the pattern
  13. Attribute Predicates Case Sensitive Case Insensitive Description startsWith iStartsWith start

    with value contains iContains contains the value anywhere endsWith iEndsWith end with value constainsWord iContainsWord contains value surrounded by whitespace (or at begin or end) notStartsWith iNotStartsWith DOES NOT start with value notContains iNotContains DOES NOT contain value anywhere notEndsWith iNotEndsWith DOES NOT end with value notContainsWord iNotContainsWord DOES NOT contain value (surrounded by whitespace, or at begin or end)
  14. Relative Traversal <div class="a"> <div class="b"> <p class="c"></p> <p class="d"></p>

    <p class="e"></p> </div> <div class="f"></div> </div> $("p.d").previous() p.c $("p.e").prevAll() p.c p.d $("p.d").next() p.e $("p.c").nextAll() p.d p.e $("p.d").parent() div.b $("p.c").siblings() p.d p.e $("div.a").children() div.b div.f $("p.d").closest(".a") div.a
  15. Navigators are Groovy Collections groovy:000> castList = [:] ===> {}

    groovy:000> b.$("table.cast_list tr").tail().each { castList[it.find("td.name").text()] = it.find("td.character").text() } ===> […] groovy:000> castList ===> {Harrison Ford=Rick Deckard, Rutger Hauer=Roy Batty, Sean Young=Rachael, Edward James Olmos=Gaff, M. Emmet Walsh=Bryant, Daryl Hannah=Pris, William Sanderson=J.F. Sebastian, Brion James=Leon Kowalski, Joe Turkel=Dr. Eldon Tyrell, Joanna Cassidy=Zhora, James Hong=Hannibal Chew, Morgan Paull=Holden, Kevin Thompson=Bear, John Edward Allen=Kaiser, Hy Pyke=Taffey Lewis} http://groovy.codehaus.org/groovy-jdk/java/util/Collection.html each() is a Groovy Collection method
  16. Forms <form id="navbar-form" … <input type="text" name="q" … groovy:000> b.q

    = "Galaxy Quest" ===> Galaxy Quest groovy:000> b.$("#navbar-form").q ===> Galaxy Quest
  17. Problem: Repetition $("a", text:"Contact Us").click() waitFor { b.title == "Contact

    Us" } $(".alert .btn-primary").click() waitFor { b.title == "Contact Us" } ($(".search-form input[name='query']") << "search term").submit()
  18. Solution: Model Pages not just DOM Browser go() : void

    quit() : void page : Page at(…) : boolean Page pageUrl : String title : String $(…) : Navigator waitFor(…) : Object verifyAt() : boolean browser: Browser Delegation class Home extends Page { static url = "" static at = { title == "IMDb - Movies, TV and Celebrities" } } groovy:000> b.$(".home").click(Home) ===> null groovy:000> b.verifyAt() ===> true groovy:000> b.at Home ===> true groovy:000> b.page ===> Home groovy:000> b.to Home ===> null
  19. Page Content class Search extends Page { static at =

    { title == "IMDb Search" } static content = { mainTable { $("#main table") } matchingTitles { mainTable[2] } matchingTitlesLinks { matchingTitles.find("a", href: contains("/title/tt")) } } } class Home extends Page { static at = { title == "IMDb - Movies, TV and Celebrities" } static url = "" static content = { boxOffice { $("h3", text:"Box Office").parent() } firstBoxOffice { boxOffice.find("a").first() } searchField { $("#navbar-query") } searchButton(to: Search) { $("#navbar-submit-button") } } }
  20. to / do / at groovy:000> b.to Home ===> null

    groovy:000> b.searchField << "Forbidden Planet" ===> [[[[[FirefoxDriver: firefox on MAC …]] groovy:000> b.searchButton.click() ===> [[[[[FirefoxDriver: firefox on MAC …]] groovy:000> b.page ===> Search
  21. Content Options static content = { boxOffice { $("h3", text:"Box

    Office").parent() } firstBoxOffice { boxOffice.find("a").first() } movieShowtimes(required:false) { $("h3", text:"Movie Showtimes").parent() } movieShowtimesGo(required:false) { movieShowtimes.find("input", value:"Go") } searchField { $("#navbar-query") } searchButton(to: Search) { $("#navbar-submit-button") } }
  22. Content Options Option Type Default Description cache boolean false Evaluate

    content once, or on each access required boolean true Error on page load if content does not exist (use false for optional or Ajax-loaded) to Page or Class, list of Page or Class null On a link, identify the page the link submits to wait varies null Wait for content to become available (via Ajax/ DHTML)
  23. Page Methods class Home extends Page { static at =

    { title == "The Internet Movie Database (IMDb)" } static url = "" static content = { boxOffice { $("h3", text:"Box Office").parent() } boxOfficeLinks { boxOffice.find("a", text: iNotStartsWith("see more")) } } def clickBoxOffice(index) { def link = boxOfficeLinks[index] def label = link.text() link.click() waitFor { title.startsWith(label) } } } groovy:000> b.to Home ===> null groovy:000> b.clickBoxOffice 0 ===> true groovy:000> Delegation to page
  24. Problem: Re-used web pages class Movie extends Page { static

    at = { title.startsWith("Blade Runner") } static content = { rating { $(".star-box-gig-start").text() } castList { $("table.cast_list tr").tail() } } } groovy:000> b.$("#main table.findList").find("a", 2).click(Movie) ===> null groovy:000> b.page ===> Movie groovy:000> b.at Movie ERROR org.codehaus.groovy.runtime.powerassert.PowerAssertionError: title.startsWith("Blade Runner") | | | false Dangerous Days: Making Blade Runner (Video 2007) - IMDb
  25. Solution: Configured Page Instances class Movie extends Page { String

    expectedTitle static at = { title.startsWith expectedTitle } static content = { rating { $(".star-box-gig-start").text() } castList { $("table.cast_list tr").tail() } } } new Movie(expectedTitle: "… movie title …")
  26. class Search extends Page { static at = { title

    == "Find - IMDb" } static content = { matchingTitleLinks { $("table.findList td.result_text > a", href: contains("/title/tt")) } } def clickMatchingTitle(int index) { def link = matchingTitleLinks[index] def label = link.text() link.click() browser.at new Movie(expectedTitle: label) } } Configured page instance, instead of page class
  27. groovy:000> b.searchField << "Blade Runner" ===> null groovy:000> b.searchButton.click() ===>

    null groovy:000> b.clickMatchingTitle 3 ===> true def clickMatchingTitle(int index) { def link = matchingTitlesLinks[index] def label = link.text() link.click() browser.at new Movie(expectedTitle: label) } click()
  28. Problem: Duplication on Pages b.$("#navbar-query") << "Bladerunner" b.$("#navbar-submit-button").click() ✦Login /

    Logout / Register ✦"Contact Us" & other boilerplate ✦"Mark Favorite" ✦View Product Details ✦Bid / Buy
  29. Solution: Modules class SearchForm extends Module { static content =

    { field { $("#navbar-query") } go(to: Search) { $("#navbar-submit-button") } } } class Home extends Page { static at = { title == "The Internet Movie Database (IMDb)" } static url = "" static content = { searchForm { module SearchForm } } } groovy:000> b.to Home ===> null groovy:000> b.searchForm.field << "Serenity" ===> [org.openqa.selenium.firefox.FirefoxWebElement@1ef44b1f] groovy:000> b.searchForm.go.click() ===> null groovy:000> b.page ===> Search
  30. Problem: Repeating Elements <table class="cast_list"> <tr> <td class="primary_photo"> … <td

    class="name"> … <td class="ellipsis"> … <td class="character"> …
  31. Solution: Module Lists class CastRow extends Module { static content

    = { actorName { $("td.name").text() } characterName { $("td.character").text() } } } class Movie extends Page { String expectedTitle static at = { title.startsWith expectedTitle } static content = { rating { $(".star-box-gig-start").text() } castList { moduleList CastRow, $("table.cast_list tr").tail() } } } Scope limited to each <tr>
  32. groovy:000> b.at new Movie(expectedTitle: "Blade Runner") ===> true groovy:000> b.castList[0].actorName

    ===> Harrison Ford groovy:000> b.castList[0].characterName ===> Rick Deckard groovy:000> b.castList*.actorName ===> [Harrison Ford, Rutger Hauer, Sean Young, Edward James Olmos, ⏎ M. Emmet Walsh, Daryl Hannah, William Sanderson, Brion James, Joe Turkel, ⏎ Joanna Cassidy, James Hong, Morgan Paull, Kevin Thompson, John Edward Allen, ⏎ Hy Pyke]
  33. js object groovy:000> b = new Browser(baseUrl: "http://jquery.org") ===> geb.Browser@5ec22978

    groovy:000> b.go() ===> null groovy:000> b.js."document.title" ===> jQuery Project Access simple page properties
  34. Executing JavaScript groovy:000> b.js.exec ''' groovy:001> $("img").css("background-color", "red").fadeOut() groovy:002> '''

    ===> null groovy:000> b.js.exec 1, 2, "return arguments[0] + arguments[1];" ===> 3 Text evaluated in-browser
  35. jQuery Hook groovy:000> b.$("img").jquery.fadeIn() ===> [org.openqa.selenium.firefox.FirefoxWebElement@de86fd70, org.openqa.selenium.firefox.FirefoxWebElement@615e6612, org.openqa.selenium.firefox.FirefoxWebElement@52c13174, org.openqa.selenium.firefox.FirefoxWebElement@69e1ba19, org.openqa.selenium.firefox.FirefoxWebElement@2797f147,

    org.openqa.selenium.firefox.FirefoxWebElement@69cfbbda, org.openqa.selenium.firefox.FirefoxWebElement@27d5741a, org.openqa.selenium.firefox.FirefoxWebElement@10b36232, org.openqa.selenium.firefox.FirefoxWebElement@ec3e243f] Silently fails unless jQuery on page More methodMissing() magic!
  36. Reporting package myapp.tests import geb.spock.* class Login extends GebReportingSpec {

    def "successful login"() { when: go "login" username = "user1" report "login screen" login().click() then: title == "Welcome, User1" } } Capture HTML and screenshot Base class that reports at end of each test ✦ reports/myapp/tests/Login/1-1-login-login screen.html ✦ reports/myapp/tests/Login/1-1-login-login screen.png ✦ reports/myapp/tests/Login/1-2-login-end.html ✦ reports/myapp/tests/Login/1-2-login-end.png
  37. Base Classes Framework Artifact Base Class Reporting Base Class Spock

    geb-spock geb.spock.GebSpec geb.spock.GebReportingSpec Junit 4 geb-juni4 geb.junit4.GebTest geb.junit4.GebReportingTest Junit 3 geb-junit3 geb.junit3.GebTest geb.junit3.GebReportingTest TestNG geb-testng geb.testng.GebTest geb.testng.GebReportingTest Report end of each test Report failures only
  38. Delegation package myapp.tests import geb.spock.* class Login extends GebReportingSpec {

    def "successful login"() { when: go "login" username = "user1" report "login screen" login().click() then: title == "Welcome, User1" } } Browser Page Geb[Reporting]Spec Delegation Delegation
  39. src/test/resources/GebConfig.groovy GebConfig.groovy import org.openqa.selenium.firefox.FirefoxDriver import org.openqa.selenium.chrome.ChromeDriver driver = { new

    FirefoxDriver() } // use firefox by default waiting { timeout = 2 // default wait is two seconds } environments { chrome { driver = { new ChromeDriver() } } } http://groovy.codehaus.org/gapi/groovy/util/ConfigSlurper.html
  40. Environment $ gradle test -Dgeb.env=chrome src/test/resources/GebConfig.groovy import org.openqa.selenium.firefox.FirefoxDriver import org.openqa.selenium.chrome.ChromeDriver

    driver = { new FirefoxDriver() } // use firefox by default waiting { timeout = 2 // default wait is two seconds } environments { chrome { driver = { new ChromeDriver() } } }
  41. Waiting Configuration GebConfig.groovy waiting { timeout = 10 retryInterval =

    0.5 presets { slow { timeout = 20, retryInterval = 1 } quick { timeout = 1 } } }