Geb und Graphene
im Vergleich

Stefan Hildebrandt / @hildebrandttk

Testen von Webanwendungen

  • Akzeptanztests
  • Funktionale Tests
  • Unit-Tests von Komponenten
  • Last- / Kapazitätstests

Beispiele

  1. Google Suche
  2. Java EE 7 Petclinic
    • Von Thomas Wöhlke auf github
    • Fachlichkeit:
      • Tierärzte mit Spezialisierungen
      • Haustiere mit Arten
      • Besitzer haben Haustieren
      • Besitzer kommen mit Haustieren zu einem Besuch
    • Fork mit Testerweiterungen auf github
Gemeinsame Basis

Selenium

Selenium Historie

  • Selenium RC: 2004
  • WebDriver: 2006
  • Merge zu Selenium 2: 2008

Selenium 2 Bindings

  • java
  • C#
  • phyton
  • ruby
  • php
  • perl
  • javascript

Selenium 2 Browserunterstützung

  • Firefox
  • Internet Explorer
  • Chrome
  • Safari
  • HTMLUnit
  • Phantom JS
  • iOS
  • Android

Geb (pronounced “jeb”)

Hint: "gebish" für Suchen

  • WebDriver
  • jQuery Selection-API
  • Groovy
  • JUnit, TestNG oder Spock
  • Release 0.4 vor 4,5 Jahren, aktuell: 0.10.0
  • gradleware-Entwickler

Arquillian

  • Von JBoss für Tests ihres AS und Frameworks entwickelt
  • Deployment des Testobjekts in einen EE Container (CDI, Servlet, Appserver)
  • Tests im Container oder als Client
  • Injection von EE-Komponenten in die Tests
  • JUnit und TestNG

Arquillian Drone & Graphene

  • Graphene & Drone sind Arquillian Extensions
  • Aus dem JBoss Umfeld
  • Drone ca. 3,5 Jahren
  • Graphene ca. 3 Jahre
Selenium

Beispiel von der Selenium Homepage:

public class Selenium2Example  {
   public static void main(String[] args) {
      WebDriver driver = new FirefoxDriver();
      driver.get("http://www.google.com");
      WebElement element = driver.findElement(By.name("q"));
      element.sendKeys("Cheese!");
      element.submit();
      System.out.println("Page title is: " + driver.getTitle());
      (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() {
         public Boolean apply(WebDriver d) {
            return d.getTitle().toLowerCase().startsWith("cheese!");
         }
      });
      System.out.println("Page title is: " + driver.getTitle());
      driver.quit();
   }
}
Selenium

Lesbarkeit

Selenium

Wiederverwendbarkeit

Selenium

Selenium Page Objects

  • Seitenstruktur
  • Bedienlogik
  • Fluent API
    ⇒ Führung bei der Testerstellung per Code Completion
  • Synchron
Selenium

Page

public class FindOwnersPage extends AbstractPage<FindOwnersPage> {

   @FindBy(id = "findOwnersForm:search")
   private WebElement search;

   @FindBy(css = "input[type='text']")
   private WebElement nameInput;

   ...

   public FindOwnersResultPage searchForOwner(String name) {
      nameInput.clear();
      nameInput.sendKeys(name);
      search.click();
      return new FindOwnersResultPage().waitForIsLoaded();
   }
}
Selenium

Test

@Test
public void testOpenNewOwnerPageFromOwnersList() {
   final FindOwnersPage findOwnersPage = new FindOwnersPage();
   findOwnersPage.get();
   findOwnersPage.assertIsLoaded()
      .searchForOwner("")
      .assertIsLoaded()
      .clickNewOwner()
      .assertPageIsLoaded();
}
Selenium

Technische Oberklasse

public abstract class AbstractPage<T extends AbstractPage<T>> extends LoadableComponent<T> {
   private static WebDriver driver;

   protected AbstractPage() {
      driver = WebDriverHolder.getDriver();
      PageFactory.initElements(driver, this);
   }

   @Override
   protected final void load() {
      getDriver().get(BASE_URL + pageUrl);
   }

   @Override
   protected void isLoaded() throws Error {
      assertTrue(getDriver().getCurrentUrl().endsWith(pageUrl));
   }
Selenium

Hilfsklasse

public class WebDriverHolder {
   private static WebDriver driver;

   public static WebDriver getDriver() {
      if (driver == null) {
            ...
            driver = new FirefoxDriver(profile);
      }
      return driver;
   }

   public static void closeDriver() {
      if (driver != null) {
         driver.quit();
         driver = null;
      }
   }

   public static void resetDriver() {
      if (driver != null) {
         driver.navigate().to("");
         if (driver.manage() != null) {
            driver.manage().deleteAllCookies();
         }
      }
   }
}
Selenium

Fazit

  • Wiederverwendbarkeit
  • Fluent-API mit Problemen
  • Eigene Framework-Klassen notwendig
  • Manuelles Setup des Browsers

Arquillian Graphene
inkl. Drone

Arquillian Drone

  • WebDriver Lifecycle inkl. Konfiguration
  • WebDriver Injection in den Test
Arquillian Drone

WebDriver Injection

@RunWith(Arquillian.class)
public class TestDroneOnly {

   @Drone
   private WebDriver driver;

   @ArquillianResource
   private URL deploymentUrl;

   @Test
   public void testOpeningHomePage() {
      driver.get(deploymentUrl + "/hello.jsf");
      assertEquals("Java EE 7 Petclinic", driver.getTitle());
   }
}
Arquillian Drone

Konfiguration in arquillian.xml

<arquillian xmlns="http://jboss.org/schema/arquillian"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://jboss.org/schema/arquillian
                                      http://jboss.org/schema/arquillian/arquillian_1_1.xsd">

   <extension qualifier="webdriver">
      <property name="browser">firefox</property>
      <!--<property name="browser">phantomjs</property>-->
      <!--<property name="browser">chrome</property>-->
   </extension>

   <extension qualifier="drone">
      <property name="instantiationTimeoutInSeconds">120</property>
   </extension>
</arquillian>

Arquillian Graphene

Arquillian Graphene

Page Object

@Location("findOwners.jsf")
public class FindOwnersPage<T extends FindOwnersPage<T>> extends AbstractFindOwnersPage<T> {

   @Page
   private FindOwnersResultPage findOwnersResultPage;

   public FindOwnersResultPage searchForOwner(String name) {
      searchForOwnerInternal(name);
      return findOwnersResultPage;
   }
}
Arquillian Graphene

Oberklasse

public abstract class AbstractFindOwnersPage<T extends AbstractFindOwnersPage<T>> {
   @FindBy(css = "input[type='text']")
   private WebElement nameInput;
   @Page
   private NewOwnerPage newOwnerPage;

   ...

   public NewOwnerPage openNewOwnersPage() {
      addNewOwnerLink.click();
      return newOwnerPage;
   }

   protected void searchForOwnerInternal(String name) {
      nameInput.clear();
      nameInput.sendKeys(name);
      search.click();
   }

   public abstract FindOwnersResultPage searchForOwner(final String s);
}
Arquillian Graphene

Test

@Test
public void testOpenNewOwnerPageFromOwnersList() {
   goTo(FindOwnersPage.class)
      .assertIsLoaded()
      .searchForOwner("")
      .assertIsLoaded()
      .openNewOwnersPage()
      .assertIsLoaded();
}
Arquillian Graphene

Fazit

  • Browser Lifecycle
  • Page und WebElement Injection
  • echte jQuery Selector-API
  • Direkte Verwendung der Selenium-API
  • Injection von Unterklasse in Oberklasse nicht möglich

Geb

Geb

geb Page

class FindOwnersPage extends AbstractPetClinicPage {

   static url ='findOwners.jsf'

   static at = { pageHeader.present }

   static content = {
      pageHeader { $('h2', id: 'findOwners') }
      nameInput { $('input', type:'text') }
      searchButton { $('input', type: 'submit') }
      addNewOwnerType { $('a', text: 'Add New Owner') }
   }

   FindOwnersResultPage searchForOwner(String name){
      nameInput.value(name)
      searchButton.click()
      return waitForAtPage(FindOwnersResultPage)
   }

   NewOwnerPage openNewOwnersPage() {
      addNewOwnerType.click()
      return waitForAtPage(NewOwnerPage)
   }
}
Geb

Technische und fachliche Oberklasse

abstract class AbstractPetClinicPage extends Page {

   static content = {
      ...
      findOwnersLink { $("a", text: "Find Owners") }
   }

   ...

   FindOwnersPage toFindOwners() {
      findOwnersLink.click()
      return waitForAtPage(FindOwnersPage);
   }

   def <T extends Page> T waitForAtPage(Class<T> targetPageClass){
               waitFor { browser.isAt(targetPageClass) }
               return browser.page as T;
   }
}
Geb

geb Test

@RunWith(Arquillian)
class Test04Owner extends GebTest {

   ...

   @Test
   public void testOpenNewOwnerPageFromOwnersList() {
      to(HelloPage)
         .toFindOwners()
         .searchForOwner('')
         .openNewOwnersPage()
}}
Geb

Konfiguration: GebConfig.groovy

baseUrl='http://localhost:8080/petclinic-all/'
driver = {
   def FirefoxProfile profile = new FirefoxProfile();
   ...
   def ffDriver = new FirefoxDriver(profile)
   ffDriver.manage().window().maximize()
   return ffDriver
}
waiting {
   timeout = 10
   retryInterval = 0.5
   presets {
      test {
         timeout = 3
         retryInterval = 0.5
      }
   }
}

environments {
   'phantomjs' {
      driver = {
         final capabilities = new DesiredCapabilities()
         capabilities.setCapability("phantomjs.page.settings.userAgent", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36")
         final phantomJSDriver = new PhantomJSDriver(capabilities)
         phantomJSDriver.manage().window().setSize(new Dimension(1028, 768))
         return phantomJSDriver
      }
   }
   chrome {
      driver = {
         final chromeDriver = new ChromeDriver()
         chromeDriver.manage().window().maximize()
         chromeDriver
      }
   }
}
Geb

Fazit

  • Page und Browser Lifecyle
  • an jQuery angelehnte Selector-API
  • Auch Selenium-API verwendbar
  • Warten auf Page in Page fehlt

Komponenten

Komponenten

  • Natürliche Komponenten
    • Tabellen inkl. Zugriff auf einzelne Zeilen und Spalten
    • Menüs
    • Gleichartige Validierung, Fehlermeldungen, ...
    • Wizzards
  • 3. Party Komponenten
    • Komplexe Inputs (Kalender, Vorschlagsboxen, ...)
    • jquery ui, PrimeFaces, RichFaces, ...
Komponenten

Graphene Page Fragments

  • Verwendung der selben Annotationen wie in der Page
  • Keine Oberklasse
  • @Root für die Basis
  • Verwendung in der Page wie WebElement
Komponenten

Graphene Page Fragment

public class OwnersTableFragment {

   @Root
   private WebElement root;

   @FindBy(css = "tbody.rf-dt-b > tr")
   private List<OwnersTableRowFragment> rows;

   public List<OwnersTableRowFragment> findRowsByParameters(String firstName, String lastName, String address,
                                                               String city, String telephone) {
      List<OwnersTableRowFragment> matchingRows = new ArrayList<>();
      for (OwnersTableRowFragment row : rows) {
         if (row.getLastName().equals(lastName) && row.getFirstName().equals(firstName)
             && row.getAddress().equals(address) && row.getCity().equals(city) && row.getTelephone().equals(telephone)) {
            matchingRows.add(row);
         }
      }
      return matchingRows;
   }
}
Komponenten

Graphene Page Fragments für 3. Party-Frameworks

Framework Verfügbarkeit von Page Fragments
Richfaces 4.5 Final
Primefaces
jQuery UI
Komponenten

geb Module

  • geb.Module analog zu geb.Page
  • Verwendung im static-Bereich mit dem Schlüsselwort: module
  • Unterstützung von Listen mit moduleList
petBirthDateInput { module RichFacesCalendar, $('#editPetForm\\:petBirthDate') }
rowsInTable { moduleList OwnersTableRowModule, $('table.table tbody tr') }
Komponenten

Geb Module

class OwnersTableRowModule extends Module {
   static content = {
      cell(required: false ) { $("td", it) }
      editOwnerLink(required: false ) { cell(0).find('a') }
      name(required: false ) { cell(0).text() }
      address(required: false ) { cell(1).text() }
      city(required: false ) { cell(2).text() }
      telephone(required: false ) { cell(3).text() }
   }

   ShowOwnerPage openDetails() {
      editOwnerLink.click()
      waitFor { browser.isAt(ShowOwnerPage) }
      return browser.page as ShowOwnerPage
   }
}
Komponenten

geb Komponentenbibliotheken für 3. Party

Einfach möglich

Existieren nicht (öffentlich)

Ajax

Ajax

Selenium

  • WebDriverWait.wait()
@FindBy(id = "owners")
private WebElement owners;
...
ExpectedCondition<Boolean> expectedCondition
            = ExpectedConditions.visibilityOfAllElements(Arrays.asList(owners));
Wait<WebDriver> wait = new WebDriverWait(driver, 3);
wait.until(expectedCondition);
Ajax

Graphene Request Guards

  • Blockiert Test für eine konfigurierte Wartezeit
  • Wirft eine Exception falls kein Request mit Response verzeichnet wurde
guardHttp(buttonWhichMakesFullPageRefresh).click();
guardAjax(buttonWhichMakesAjaxRequest).click();
guardNoRequest(buttonWhichMakesNoRequest).click();
Ajax

Graphene 2 Waitings

  • Fluent API
  • Referenzierung von WebElements
button.click();
waitGui()
   .withMessage("Popup should be opened after clicking on that button!")
   .until().element(popupPanel).is().visible();
Ajax

geb WaitingSupport

  • durch Closure sehr flexibel
  • Konfiguration von Defaults in GebConfig.groovy
waitFor {}
waitFor(message: 'Der neue Datensatz ist nicht erschienen'){}
waitFor(10) {}
waitFor(10, 0.5) {}
waitFor(message: 'Der neue Datensatz ist nicht erschienen', 'quick') {}
waitFor { theResultDiv.present }
Ajax

geb lazy content

class DynamicPage extends Page {
    static content = {
        dynamicallyAdded(wait: true) { $("p.dynamic") }
    }
}

Browser.drive {
    to DynamicPage
    assert dynamicallyAdded.text() == "I'm here now"
}
  • Es wird beim Zugriff automatisch gewartet
  • Timing kann konfiguriert werden

Programmierstiel & Lesbarkeit

Programmierstiel & Lesbarkeit

Selenium Page Objects

  • Annotation für Java EE Entwickler gewohnt
  • Ordentliches Selector-API
  • Keine Unterstützung für Komponenten
  • Explizites Wireing an diversen Stellen
  • WebDriver-API ist in die Jahre gekommen
  • Schwierigkeiten bei Wait auf Page-Felder zu referenzieren
Programmierstiel & Lesbarkeit

graphene & drone

  • Annotation für Java EE Entwickler gewohnt
  • Verbessertes Selektor-Api
  • Größtenteils transparentes Wiring
  • Waiting API ermöglicht Referenzierung von Feldern
  • WebDriver-API ist in die Jahre gekommen
Programmierstiel & Lesbarkeit

geb

  • Transparentes Wiring
  • Verbesserte Selektor-Api
  • Konfiguration z.B. für wait()
  • Groovy-Power-Assertions
  • groovy
  • groovy
  • static-Bereich
  • teilweise untypisiert

Kombinationsmöglichkeiten

Kombinationsmöglichkeiten

  • Arquillian
  • Arquillian Suite Deployments
  • Arquillian Warp
  • Cucumber / fit
  • ArquillianCucumber
  • jMeter

Testausführung mit Arquillian

Testausführung mit Arquillian

Selenium

  • Kein eigener Testrunner
  • Keine Interferenzen mit Arquillian-Suite
Testausführung mit Arquillian

Arquillian Graphene

  • Arquillian-Extension
  • Keine Interferenzen mit Arquillian-Suite
Testausführung mit Arquillian

geb

  • Kein eigener Testrunner
  • Keine Interferenzen mit Arquillian-Suite

Testausführung mit Cucumber

Testausführung mit Cucumber

Selenium

  • Etwas LifeCycle Glue-Code
  • ArquillianCucumber
public class WebDriverHolderCucumberLifeCycle {

   @After(order = 1)
   public static void resetDriver() {
      WebDriverHolder.closeDriver();
   }
}
Testausführung mit Cucumber

Arquillian Graphene

  • Ohne Arquillian-Testrunner nicht lauffähig
  • Page Injection mit ArquillianCucumber fehlerhaft
Testausführung mit Cucumber

geb

  • Etwas Glue-Code
  • ArquillianCucumber
class GebStepDefinitions {
   String gebConfEnv = null
   String gebConfScript = null

   private Browser _browser

   Configuration createConf() {
      new ConfigurationLoader(gebConfEnv, System.properties, new GroovyClassLoader(getClass().classLoader)).getConf(gebConfScript)
   }

   Browser createBrowser() {
      new Browser(createConf())
   }

   Browser getBrowser() {
      if (_browser == null) {
         _browser = createBrowser()
      }
      _browser
   }

   @After
   void resetBrowser() {
      if (_browser?.config?.autoClearCookies) {
         _browser.clearCookiesQuietly()
      }
      _browser = null
   }

   def methodMissing(String name, args) {
      getBrowser()."$name"(* args)
   }

   def propertyMissing(String name) {
      getBrowser()."$name"
   }

   def propertyMissing(String name, value) {
      getBrowser()."$name" = value
   }
}

Performance / Capacity Tests mit Selenium

Performance / Capacity Tests mit Selenium

pro

  • Korrekte Bedienung
  • "Echte" Last
  • Revisionssichere Pflege durch fachliche Tests
  • HtmlUnit oder Phantom JS
Performance / Capacity Tests mit Selenium

contra

geb mit jMeter

geb mit jMeter
  • Mit etwas Glue-Code
abstract class AbstractGebSamplerClient extends AbstractJavaSamplerClient implements Serializable {

   @Override
   Arguments getDefaultParameters() {
      return new Arguments()
   }

   @Override
   void setupTest(final JavaSamplerContext context) {
      to(HelloPage)
   }

   @Override
   void teardownTest(final JavaSamplerContext context) {
      resetBrowser()
   }

   String gebConfEnv = null
   String gebConfScript = null

   private Browser _browser

   Configuration createConf() {
      new ConfigurationLoader(gebConfEnv, System.properties, new GroovyClassLoader(getClass().classLoader)).getConf(gebConfScript)
   }

   Browser createBrowser() {
      new Browser(createConf())
   }

   Browser getBrowser() {
      if (_browser == null) {
         _browser = createBrowser()
      }
      _browser
   }

   void resetBrowser() {
      if (_browser?.config?.autoClearCookies) {
         _browser.clearCookiesQuietly()
      }
   }

   def methodMissing(String name, args) {
      getBrowser()."$name"(*args)
   }

   def propertyMissing(String name) {
      getBrowser()."$name"
   }

   def propertyMissing(String name, value) {
      getBrowser()."$name" = value
   }
}
Selenium Page Objects Arquillian Graphene geb
Arquillian Deployment
Arquillian Suite Deployment
Arquillian Warp
Cucumber
Cucumber mit Arquillian Deployment
Cucumber mit Arquillian Suite Deployment
jMeter

Geschwindigkeit

Geschwindigkeit

Testausführung

Stabilität

Stabilität

  • Keine grundsätzlichen Unterschiede

Goodies

Goodies
Selenium Page Objects Arquillian Graphene geb
Vereinfachte Screenshoots -
Javascript einfacher im Browser ausführen -
Download
Konfiguration von Browser
Konfiguration von Timings
Angepasste Selektoren
jQuery Selektoren
AngularJS

Auswahl Kriterien

Auswahl Kriterien

  • graphene
    • Java EE - Server
    • Große Basis existierender Selenium Page Objects
    • Außerhalb von Unit-Tests nicht einsetzbar
  • geb
    • Kombinationsmöglichkeiten
    • "Andere Sprache"

Slides

h9t.eu/s/gugiv

Stefan Hildebrandt - consulting.hildebrandt.tk

  • Beratung, Coaching und Projektunterstützung
  • Java EE
  • Buildsysteme gradle und maven/ant-Migration
  • Testautomatisierung
  • Coach in agilen Projekten
  • DevOps

Links

Datenschutz Impressum