Share
Embed
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
Transcript
77 slides
-
Testing Web Applications with GEB Howard M. Lewis Ship TWD
Consulting [email protected] Twitter: @hlship © 2013 Howard M. Lewis Ship -
How tough is it? Request Handler Response <html> <head> …
<body> … </body> </html> -
HTML Markup JavaScript Ajax HTTP Request Dispatch Templates Database Synchronization
User Interaction Controllers Session State -
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 -
Spoiled by jQuery $(".order-summary .subtotal").text() == "107.95" <div class="order-summary"> …
<div class="subtotal">107.95</div> http://jquery.com/ -
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 -
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 -
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 -
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 -
✦Power of Selenium WebDriver 2 ✦Elegance of jQuery content selection
✦Robustness of Page Object modelling ✦Expressiveness of Groovy ✦First Class Documentation -
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 -
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> -
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 -
✦$("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 -
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 -
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) -
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 -
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 -
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 -
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() -
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 -
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") } } } -
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 -
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") } } -
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) -
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 -
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 -
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 …") -
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 -
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() -
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 -
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 -
Problem: Repeating Elements <table class="cast_list"> <tr> <td class="primary_photo"> … <td
class="name"> … <td class="ellipsis"> … <td class="character"> … -
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> -
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] -
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 -
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 -
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! -
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 -
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 -
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 -
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 -
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() } } } -
Waiting Configuration GebConfig.groovy waiting { timeout = 10 retryInterval =
0.5 presets { slow { timeout = 20, retryInterval = 1 } quick { timeout = 1 } } }