Suche
UI-Test mit Selenium WebDriver

Automatisiertes Testen von Weboberflächen

Andreas Monschau

© Shutterstock/rzoze19

Testautomatisierung als solche ist mittlerweile in den meisten Softwareprojekten angekommen, sofern die Entwicklung nicht noch auf dem Stand der Neunzigerjahre des vergangenen Jahrhunderts stehen geblieben ist. So werden z. B. in Komponententests einzelne Klassen getestet, in Integrationstest wird das Zusammenspiel mehrerer Komponenten betrachtet usw. Bei den meisten Entwicklern sind Begriffe wie Mocking, TDD, JUnit, Arquillian etc. mittlerweile angekommen, wobei gerne noch (auch genormte) Begriffe durch die Projektbeteiligten durcheinander geworfen werden. Für den Test von Weboberflächen gehört im Java-Umfeld Selenium WebDriver zu den weit verbreiteten Testwerkzeugen. Dieser Artikel soll einen ersten Einstieg bieten.

Automatisierte UI-Tests von Weboberflächen zielen, wie es auch bei automatisierten Komponenten- und Integrationstests der Fall ist, u. a. darauf ab, erwartete Ergebnisse mit den tatsächlichen zu vergleichen (Soll-Ist-Vergleich), um somit eine verlässliche Aussage darüber treffen zu können, ob das Testobjekt (in diesem Fall die zu testende Webseite) gemäß einer vorliegenden Spezifikation implementiert wurde und dieses Testobjekt bestimmte Qualitätsmerkmale aufweist. Es existieren einige wichtige Argumente, die für eine Automatisierung von UI-Tests sprechen; hervorzuheben ist da zum einen, dass sich automatisierte Testfälle mit hoher Geschwindigkeit ausführen lassen können; schneller als es jede studentische Hilfskraft vermag. Darüber hinaus sorgt ein automatisierter UI-Test für einen kleinen „Integrationstest“ – von oben (der Weboberfläche) bis ganz nach unten (Service oder Datenbank) und anschließend wieder zurück – und gibt so grundsätzliche Auskunft über den Zustand der Anwendung. Dem gegenüber stehen wiederum hohe Implementierungsaufwände in der Einführung, sowie stetiger Aufwand im Anpassen der Testskripte aufgrund von Änderungen im Testobjekt, wobei diese Begründungen jedwede Art von Testautomatisierung in Frage stellen würden.

API Summit 2018
Christian Schwendtner

GraphQL – A query language for your API

mit Christian Schwendtner (PROGRAMMIERFABRIK)

Für den automatisierten Test von Webseiten sollten darüber hinaus unter anderem mindestens folgende Anforderungen bestehen:

  • Die Testautomatisierung soll verschiedene Browser unterstützen. Die Nutzer der Webseite benutzen nicht nur einen einzigen Browser. Stellen Sie sich ein soziales Netzwerk vor, dass nur für den Internet Explorer verfügbar ist – undenkbar. Bei Spezialanwendungen mag es natürlich eine spezifizierte Vorgabe bezüglich des zu nutzenden Browsers geben, da sich dies aber auch von Anwendung zu Anwendung wieder unterscheidet, sollte die Testautomatisierung eine Vielzahl von Browsern (zumindest die gängigsten) unterstützen.
  • Asynchrones Verhalten eines Testobjekts soll getestet werden können. Änderungen in der Weboberfläche durch spezifisches Nachladen einzelner Inhalte sollen ebenfalls durch die Testautomatisierung berücksichtigt werden können.
  • Die Durchführung von Regressionstests soll möglich sein. Eine Webseite soll nach einer Änderung jederzeit erneut getestet werden können, um nachzuweisen, dass die bestehenden Funktionalitäten weiterhin Bestand haben und keine neuen Fehlerzustände hinzugefügt wurden.

Diese Liste ließe sich annähernd beliebig fortsetzen. Mit Selenium existiert ein Werkzeug, das diese Anforderungen erfüllen kann. Es handelt sich dabei um eine freie Software und wird unter der Apache-2.0-Lizenz veröffentlicht. Das Projekt aggregiert mehrere Softwaretools, wobei jedes eine spezielle Rolle erfüllt. Neben der bekannten Selenium-IDE, einer Extension für den Mozilla-Firefox-Browser in Form eines Capture-and-Replay-Tools, stellt Selenium WebDriver (auch Selenium 2 genannt) den vielleicht wichtigsten Bestandteil des Selenium-Projekts dar. Selenium WebDriver ermöglicht es u. a., automatisierte Testfälle zu erstellen und diese für Regressionstests zu nutzen. Entwickelt wird Selenium von der Firma ThoughtWorks. Für die Beispiele dieses Artikels wird Selenium WebDriver in der Version 2.47.1 verwendet.

Merkmale von Selenium WebDriver

Selenium WebDriver verfügt unter anderem über folgende wichtige Merkmale:

  • Leichte Erlernbarkeit: Für einen Java-Entwickler ist Selenium WebDriver leicht erlernbar. Man bindet die Abhängigkeit in die Maven pom.xml (Listing 1) ein und kann direkt loslegen. Nach wenigen Minuten stellen sich die ersten Erfolgserlebnisse ein.
(...)
<dependency>
  <groupId>org.seleniumhq.selenium</groupId> 
  <artifactId>selenium-java</artifactId> 
  <version>2.47.1</version> 
</dependency>
(...)
  • Verfügbarkeit für verschiedene Programmiersprachen: Das Testwerkzeug steht für eine Vielzahl an Programmiersprachen zur Verfügung. Am bekanntesten ist die Kombination mit Java, aber es existiert auch die Möglichkeit, Selenium WebDriver u. a. in C#, Python oder Ruby zu verwenden.
  • Unterstützung gängiger Browser: Selenium WebDriver unterstützt unter anderem Google Chrome, Internet Explorer, Mozilla Firefox, Safari und Opera. Grundvoraussetzung für die Verwendung eines spezifischen Browsers ist, dass dieser auf dem Testsystem bzw. in der Testumgebung installiert ist. Die Beispiele dieses Artikels zielen auf die Verwendung von Mozilla Firefox ab, sollten aber auch mit jedem beliebigen anderen Browser ausgeführt werden können.
  • Testausführung in eigenständigen Browserinstanzen: Sobald ein Test, welcher Selenium WebDriver verwendet, gestartet wird, öffnet sich eine neue Instanz des gewünschten Browsers, d. h., es wird ein Browserfenster geöffnet und der ausführende Tester kann dem Test, welcher unmittelbar beginnt, bei der Durchführung zuschauen. Für jeden unterstützten Browser existieren so genannte Driver-Klassen; dabei handelt es sich um spezifische Implementierungen des Interface WebDriver.

Selenium WebDriver ruft den jeweiligen Browser, der für den Test verwendet werden soll, direkt auf und nutzt dabei die jeweilig bereitgestellte Unterstützung für Automatisierung. Wie diese Aufrufe konkret ausgeführt werden, hängt vom jeweilig verwendeten Browser ab.

Grundsätzlich imitiert Selenium WebDriver das Verhalten eines menschlichen Nutzers bzw. dessen Interaktion mit dem Testobjekt. Einzelne Bestandteile dieses Testobjekts, die HTML-Elemente (das sind z. B. Eingabefelder eines HTML-Formulars oder auch ein Verweis, welcher zur nächsten HTML-Seite des Testobjekts verlinkt), werden durch WebElement-Objekte repräsentiert. Die offizielle Dokumentation stellt eine Übersicht über die für Webelemente bereitgestellten Methoden zur Verfügung.

Die jeweilig verwendete WebDriver-Implementierung bietet die Methode findElement() an, welche es ermöglicht, Webelemente zu lokalisieren. Listing 2 liefert ein Beispiel für Lokalisierung und Interaktion mit einem Webelement.

WebElement formSearchButton = driver.findElement(By.id("formsearchbutton"));
searchButton.submit();

Der WebDriver-Methode findElement() wird als Argument ein Objekt des Typs By übergeben, wobei diese Klasse verschiedene Möglichkeiten bereitstellt, ein Element auf einer Webseite zu lokalisieren, z. B. anhand der eindeutigen ID eines Webelements (wie in Listing 1 der HTML-Formularbutton mit dem ID-Attribut mit dem Wert formsearchbutton) oder auch anhand des Name-Attributs. Des Weiteren ist es möglich und manchmal auch nötig, ein Webelement mithilfe von XPath (wie auch im zweiten Beispiel dieses Artikels), via CSS o. Ä. zu lokalisieren. Die Dokumentation von Selenium WebDriver beschreibt alle Varianten im Detail.

Testobjekt für das Beispiel

Bei dem Testobjekt handelt es sich um eine einfach gehaltene Webanwendung, die eine filterbare Kundenliste liefert. Um das Beispiel übersichtlich zu halten, wurden bei der Erstellung des Testobjekts komplexe asynchrone Vorgänge vermieden. Das Testobjekt besteht aus einer Startseite (Abb. 1) und einer filterbaren Kundenliste (Abb. 2). Der Filter für die Kundenliste besteht aus einem Formular, welches Radiobuttons für die einzelnen Filterkriterien sowie einen Button zum Absenden beinhaltet.

Abb. 1: Startseite des Testobjekts

Abb. 1: Startseite des Testobjekts

Abb. 2: Vollständige Kundenliste

Abb. 2: Vollständige Kundenliste

Vorgehensweise

Wenn man JUnit verwendet, um Selenium WebDriver zu nutzen, so stehen im Wesentlichen zwei Wege zur Verfügung, um mit einem Testobjekt zu interagieren: Nutzung von Selenium WebDriver direkt aus einer JUnit-Klasse heraus oder Kapseln der Selenium-WebDriver-Funktionalitäten in Page-Object-Klassen, wobei JUnit dann nur mit diesen Objekten arbeitet. Der Kasten „Page Object Pattern“ beschreibt das gleichnamige Pattern. Da sich dieses Entwurfsmuster bereits in verschiedenen Projekten bewährt hat, wird es auch in dem hier gezeigten Beispiel verwendet.

Page Object Pattern
Ein Page Object kapselt eine HTML-Seite oder ein darin enthaltenes Fragment mit einem anwendungspezifischen API, welches es erlaubt, (JUnit-)Testmethoden zu schreiben, ohne direkt HTML ansprechen zu müssen. Bei der Navigation liefert die initiale Seite ein neues Page Object für die neue Seite zurück. Assertions können im Page Object oder im Testskript ausgeführt werden, die Beispielanwendung dieses Artikels implementiert die Assertions in den Page-Object-Klassen. Eine sehr gute Übersicht über das Page Object Pattern bieten zum einen Martin Fowler und die Selenium-WebDriver-Dokumentation.

Betrachten wir zunächst die (auf das Wesentliche reduzierte) Klasse StartPage.java, welche die Startseite repräsentiert (Listing 3). In dieser Klasse wird das Verhalten des konkreten Testobjekts beschrieben und ermöglicht uns Interaktion mit ihr. Diese Interaktion beschränkt sich aber zunächst nur darauf, dass der Titel der im Browser geöffneten Startseite zurückgegeben wird und zur Kundenliste navigiert werden kann. Die Methode navigateToCustomerList() liefert ein Objekt der Klasse CustomerListPage zurück (Listing 4).

Mit dem gelieferten Objekt der CustomerListPage-Klasse ist nun etwas mehr Interaktion möglich. Durch Aufruf der Methode setFilter() wird ein festgelegtes Filterkriterium ausgewählt (Abb. 2) und durch Verwendung der WebElement-Methode submit() das Formular an den Server abgesendet.

public class StartPage {
  
  private final WebDriver webDriver;
  //Locator für den Link zur Kundenübersicht
  By linkToCustomerListLocator = By.id("customerList");
  
  public StartPage(WebDriver webDriver){
    this.webDriver = webDriver;
  }
  
  //Diese Methode liefert den Titel des Testobjekts zurueck
  public String getStartPageTitle(){
    return this.webDriver.getTitle();
  }
  
  // Das Betätigen des Links mit der ID "customerList" leitet uns weiter zur
  // Kundenübersicht, diese Methode liefert das entsprechende PageObject.
  public CustomerListPage navigatoToCustomerList(){
    webDriver.findElement(linkToCustomerListLocator).click();
    return new CustomerListPage(webDriver);
  }
}

Insbesondere bei der Ausführung von Aktionen wie Filterung von auszugebenden Ergebnissen, kann es notwendig sein, auf eine Aktualisierung des Testobjekts zu warten. Oft passiert es auch, dass eine Seite nicht korrekt geladen oder das gesuchte Webelement unter bestimmten Umständen noch gar nicht erzeugt wurde. Um das Verhalten von Selenium WebDriver an dieser Stelle etwas präziser zu steuern, kann man implizites und explizites Warten konfigurieren. In der offiziellen Dokumentation werden beide Varianten mit umfangreichen Beispielen ausführlich beschrieben. Bezugnehmend auf asynchrones Verhalten von Teilen eines Testobjekts sei an dieser Stelle auch auf WebDriverWait und ExpectedConditions verwiesen.

Die Methode getCustomerTableCount() gibt als Ergebnis die Anzahl der Zeilen der Tabelle mit den Kundendaten zurück. Sie bedient sich dazu eines XPath-Ausdrucks zur Lokalisierung der Tabelle. Die Methode findElements() gibt eine Liste von WebElementen zurück, im Falle des Beispiels sind in dieser Liste alle „Tabellenzeilen“-Webelemente abgelegt, und da ausschließlich die Anzahl der Elemente in diesem Fall relevant ist, muss lediglich auf die Größe der Liste geschaut werden (Listing 4).

public class CustomerListPage { 
  private final WebDriver webDriver;
  
  By radioButtonLocator = By.id("value_Ectomorf"); 
  By submitFilterButtonLocator = By.id("filterButton");
  
  
  public CustomerListPage(WebDriver webDriver){ 
    this.webDriver = webDriver; 
  }
  
  
  public int getCustomerTableCounter(){ 
    return webDriver.findElements(By.xpath("//table[@id='customerListTable']/
      tbody/tr")).size(); 
  }
  
  public CustomerListPage setFilter(){ 
    this.webDriver.findElement(radioButtonLocator).click(); 
    this.webDriver.findElement(submitFilterButtonLocator).submit(); 
    return new CustomerListPage(webDriver); 
  }

}

Das Testskript

Bei dem Testskript handelt es sich um eine mit @Test annotierte Methode einer JUnit-Klasse. Der Workflow ist recht einfach gehalten:

  1. Öffne die Startseite
  2. Prüfe den Titel der Startseite
  3. Navigiere zur Kundenliste
  4. Prüfe, ob acht Kundendatensätze angezeigt werden
  5. Filtere explizit auf ein Kundenunternehmen
  6. Prüfe, ob nach Filterung nur noch vier Kundendatensätze angezeigt werden

Es wird eine Reihe von Positivtests ausgeführt, Negativtests wurden außen vor gehalten, generell wäre es aber auch möglich, diese Art von Tests durchzuführen und z. B. das Testobjekt auf Robustheit zu prüfen. Das Testskript (Listing 5) erzeugt zunächst eine neue WebDriver-Instanz driver der Ausprägung FirefoxDriver und setzt das implizite Warten des Objekts auf zehn Sekunden (s. u.). Die WebDriver-Methode get() öffnet den übergebenen URL. Nach dem Öffnen der Startseite wird eine erste Zusicherung getestet, und zwar ob der Titel der geöffneten Webseite auch tatsächlich der Zeichenkette „Telculator – Calculate the Telecommunication“ gleicht. Anschließend wird im Testobjekt weiter zur Kundenliste navigiert, indem man die Methode navigateToCustomerList() aufruft. Hinweis: Würde man in der in Listing 3 gezeigten Klasse StartPage den Wert des By-Objekts linkToCustomerListLocator von customerList z. B. auf customerList1 ändern, so würde Selenium WebDriver dies, nach Ablauf der durch driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS) festgelegten Zeit von zehn Sekunden bis zum Timeout der Suche nach dem Webelement, mit einer ElementNotFoundException quittieren.

Mit dem durch navigateToCustomerList() gelieferten CustomerListPage-Objekt werden dann die restlichen Aktivitäten und Tests durchgeführt: Zunächst wird die Anzahl der Datensätze in der ungefilterten Tabelle geprüft (die Anzahl sollte acht betragen), anschließend wird die Filterung ausgeführt, was zu einer geänderten Ansicht führt (Abb. 3). Darauf folgt die Prüfung, ob die Filterung erfolgreich war, denn nun sollte die Zählung der Tabellenzeilen ergeben, dass nur noch vier Zeilen ausgegeben werden (eine inhaltliche Prüfung wäre zusätzlich natürlich auch möglich, würde aber über den Umfang dieses Artikels hinausgehen).

Abb. 3: Gefilterte Kundenliste

Abb. 3: Gefilterte Kundenliste

(...)
private static final int FILTERED_CUSTOMER_COUNT = 4;
private static final int UNFILTERED_CUSTOMER_COUNT = 8;
private static final String STARTPAGE_TITLE = "Telculator - Calculate the Telecommunication";
(...)
WebDriver driver;
driver = new FirefoxDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.get("URL des Testobjekts");
     

   
StartPage startPage = new StartPage(driver);
Assert.assertEquals(STARTPAGE_TITLE, startPage.getStartPageTitle());

CustomerListPage customerListPage = startPage.navigatoToCustomerList();
Assert.assertEquals(UNFILTERED_CUSTOMER_COUNT, 
  customerListPage.getCustomerTableCounter());

customerListPage = customerListPage.setFilter();
Assert.assertEquals(FILTERED_CUSTOMER_COUNT, 
  customerListPage.getCustomerTableCounter());

driver.quit();
(...)

Der Aufruf der Methode quit() „beendet“ die Instanz driver und schließt alle dazugehörigen, geöffneten Browserfenster.

Wenn keine Fehlerwirkungen aufgetreten sind, quittiert JUnit den Testlauf als erfolgreich und der ausführende Tester (bzw. derjenige, der für das Ablesen von Testergebnissen in z. B. Jenkins bestimmt wurde, sofern das nicht automatisiert geschieht) vermerkt das Testergebnis im entsprechenden Werkzeug für die Testaktivität Berichterstattung bzw. Fortschrittsbericht.

Fazit

Die bereits zu Beginn vorgestellten Punkte, die für einen automatisierten Test sprechen, konnten direkt mit Ergebnissen untermauert werden:

  • Der Testfall lässt sich in sehr hoher Geschwindigkeit ausführen (je nach Testumgebung so schnell, dass man es kaum nachverfolgen kann – um jeden einzelnen Testschritt visuell nachzuvollziehen, könnte man, dann aber natürlich nur testweise, den Code z. B. um Thread.sleep() o. Ä. ergänzen).
  • Der „kleine Integrationstest“ findet statt – die Navigation auf der Seite erfordert die korrekte Implementierung im Backend, wenn z. B. die Kundenliste geliefert wird.

Durch Verwendung des Page Object Pattern ergibt sich ein weiterer Vorteil – Änderungen an der Oberfläche können direkt an den Page-Object-Klassen nachgezogen werden, das eigentliche Testskript bleibt unberührt.

Für die Punkte, die gegen eine Automatisierung sprechen, liegen die Argumente natürlich auf der Hand – der Aufwand für diesen, zugegeben recht kleinen, Testfall ist recht hoch, und Änderungen können zwar einfach nachgezogen werden, aber dies muss konsequent geschehen, was weitere stetige Aufwände in der Wartung bedeutet. Für das mit dem Testen beauftragte Personal ist demnach eine der ersten Aufgaben, das Testobjekt und die Anforderungen dahingehend zu prüfen, was sich „sinnvoll automatisieren lässt“ und was man vielleicht doch jemanden manuell testen lassen sollte.

Ob man nun das Page Object Pattern nutzt oder nicht, ist abhängig davon, was genau getestet werden soll. Führt man Selenium WebDriver für ein spezifisches Projekt ein und kennt genau die Testobjekte, so bietet sich das Entwurfsmuster an. Möchte man jedoch eine unternehmensweite, produktübergreifende UI-Testautomatisierung aufbauen, so ergibt es Sinn, einen generischeren Ansatz zu wählen.

Dieser Artikel kann natürlich nur einen allgemeinen Überblick über das Testwerkzeug bieten – einige Themen bleiben natürlich auf der Strecke , z. B. Vertiefung der Thematik „implizites und explizites Warten“, Umgang mit asynchronem Verhalten eines Testobjekts, der HtmlUnitDriver oder ein Ausblick auf verteilte Lösungen mit Selenium Grid, Letzteres ist ein Bestandteil von Selenium WebDriver. Prinzipiell sollte es Ihnen aber nun, aufgrund der Beispiele und der weiterführenden Links, möglich sein, erste Testskripts für Ihre eigenen Testobjekte implementieren zu können.

Abschließend lässt sich sagen, dass Selenium WebDriver schnell zu brauchbaren und aussagekräftigen Ergebnissen führt. Die Arbeit mit diesem Testwerkzeug ist leicht erlernbar, die Einarbeitungszeit ist überschaubar, darüber hinaus ist es recht stabil und robust, und bringt Sie dem Ziel, qualitativ hochwertige Software abzuliefern, ein gutes Stück näher.

Aufmacherbild: Digital tablet pc von Shutterstock / Urheberrecht: rzoze19

Geschrieben von
Andreas Monschau
Andreas Monschau
Andreas Monschau ist als IT Consultant mit den Schwerpunkten Java-Softwareentwicklung und Softwaretest (Management, Automatisierung) bei Haeger Consulting in Wachtberg bei Bonn tätig. Seine sonstigen Interessen wechseln stetig, derzeit sind es PHP7, arc42 und Microservices. Nebenher schreibt er Fachartikel und hält Vorträge. Twitter: @andreasmonschau
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: