Slide 1

Slide 1 text

Testing Web Applications with GEB Howard M. Lewis Ship TWD Consulting [email protected] Twitter: @hlship © 2013 Howard M. Lewis Ship

Slide 2

Slide 2 text

Testing Web Applications

Slide 3

Slide 3 text

How tough is it? Request Handler Response … …

Slide 4

Slide 4 text

Grab It, Compare It, Done! … Right?

Slide 5

Slide 5 text

HTML Markup JavaScript Ajax HTTP Request Dispatch Templates Database Synchronization User Interaction Controllers Session State

Slide 6

Slide 6 text

Browser ➠ Integrated View

Slide 7

Slide 7 text

Page Navigation

Slide 8

Slide 8 text

DOM Traversal

Slide 9

Slide 9 text

User Interaction

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Spoiled by jQuery $(".order-summary .subtotal").text() == "107.95"
107.95
http://jquery.com/

Slide 12

Slide 12 text

✦Drives: ✦or limited HtmlUnit (browserless) Selenium WebDriver http://seleniumhq.org/

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }; Triggers Ajax Update Triggers Ajax Update

Slide 17

Slide 17 text

Java ➠ High Ceremony

Slide 18

Slide 18 text

Groovy ➠ Low Ceremony + Dynamic

Slide 19

Slide 19 text

✦Power of Selenium WebDriver 2 ✦Elegance of jQuery content selection ✦Robustness of Page Object modelling ✦Expressiveness of Groovy ✦First Class Documentation

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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>

Slide 22

Slide 22 text

Google Demo

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

IMDB Demo

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

✦$("p") ➠ all

✦$("p", 3) ➠ 4th

✦$("p")[3] ➠ 4th

✦$("p")[0..2] ➠ 1st through 3rd

$(css selector, index, attribute / text matchers) CSS3

Slide 29

Slide 29 text

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)

Slide 30

Slide 30 text

Relative Traversal

$("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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Forms b.q = "Galaxy Quest" ===> Galaxy Quest groovy:000> b.$("#navbar-form").q ===> Galaxy Quest

Slide 33

Slide 33 text

Pages and Modules

Slide 34

Slide 34 text

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()

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Pages Demo

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

Page Content Demo

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

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") } } }

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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") } }

Slide 43

Slide 43 text

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)

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Page Methods Demo

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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 …")

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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()

Slide 51

Slide 51 text

Configured Page Demo

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Problem: Repeating Elements … … … …

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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]

Slide 58

Slide 58 text

Client-Side JavaScript

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Client JavaScript Demo

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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!

Slide 64

Slide 64 text

Testing Framework Integration

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Configuration

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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() } } }

Slide 71

Slide 71 text

Waiting Configuration GebConfig.groovy waiting { timeout = 10 retryInterval = 0.5 presets { slow { timeout = 20, retryInterval = 1 } quick { timeout = 1 } } }

Slide 72

Slide 72 text

More Info

Slide 73

Slide 73 text

http://www.gebish.org

Slide 74

Slide 74 text

https://github.com/geb/geb

Slide 75

Slide 75 text

http://howardlewisship.com

Slide 76

Slide 76 text

Q & A

Slide 77

Slide 77 text

http://www.flickr.com/photos/hlship/4328262153/ http://www.flickr.com/photos/charlestilford/189670488/ http://www.flickr.com/photos/druclimb/370334116/ http://www.flickr.com/photos/shivenis/3708942151/ creative commons