09. März 2017

synTechTalk: Selenium – Web Browser Automation in Java

Selenium – Web Browser Automation in Java (Bild: http://www.seleniumhq.org/)

Dieser Blogpost richtet sich an alle, die dabei sind, die Nutzung von Selenium zu erlernen oder die ein generelles Interesse an Browser-Automatisierung haben. Anhand eines kleinen praktischen Beispiels wird demonstriert, wie Selenium zum Testen von Webseiten genutzt werden kann. Als Voraussetzung werden Java-Grundkenntnisse angenommen.

Inhaltsverzeichnis
1. Was kann Selenium?
2. Wie funktioniert Selenium?
3. Testen mit Selenium WebDriver
4. Page Object Design Pattern

1. Was kann Selenium?

Seleniums Hauptfeature ist die Browser-Automatisierung. Diese unterteilt sich in zwei Teile – einmal die Selenium IDE und einmal den Selenium WebDriver. Mit der IDE lassen sich im Browser Aktionen „aufnehmen“. Diese Aufnahmen werden in der eigenen Skriptsprache „Selenese“ gespeichert und können beliebig wiedergegeben werden. Dies ermöglicht die Automatisierung von Abläufen im Browser.

Der WebDriver ist dazu gedacht, programmatisch die nativen Steuerungsmechanismen verschiedener Browser zu bedienen. Dadurch ergibt sich neben der Automatisierung genereller Tasks auch die Anwendung in (Regressions-) Tests.

Zurzeit unterstützt der WebDriver folgende Browsertreiber:

  • HtmlUnitDriver, steuert einen auf HtmlUnit basierenden Java-Browser ohne UI
    → schnellste und einfachste Variante
  • Firefox Driver
  • Internet Explorer Driver
  • Chrome Driver
  • Opera Driver

Außerdem existieren eigene Projekte für Android (Selendroid) und iOS (appium).

2. Wie funktioniert Selenium?

In einem ersten Schritt wird eine Seite aufgerufen. Dafür muss zunächst ein Driver definiert werden.

WebDriver driver = new HtmlUnitDriver();

Auf diesem Driver ruft man „get“ auf.

WebDriver driver = new HtmlUnitDriver();
driver.get("http://www.example.com");

Hat der Driver die Seite geladen (dazu später mehr), kann man auf verschiedene Art und Weise Elemente auswählen. Diese befinden sich in der „By“-Klasse und heißen wie folgt:

  • By.className – Findet ein Element anhand des class Attributs.
  • By.cssSelector – Findet ein Element mithilfe CSS.
  • By.id – Findet ein Element anhand der id.
  • By.tagName – Findet ein Element anhand des HTML-Tags.
  • By.name – Findet ein Element anhand seines name Attributs.
  • By.linkText – Findet ein Element anhand seines sichtbaren Linktextes.
  • By.partialLinkText – Findet ein Element anhand des Linktextes, der das Angegebene enthält.
  • By.xpath – Findet ein Element via XPath.

Zusätzlich kann jederzeit Javascript ausgeführt werden, um gezielt Elemente auszuwählen.

Weitere wichtige WebElement Methoden sind:

  • element.getText() – Liefert den sichtbaren Text eines Elements.
  • element.sendKeys(„text“) – Schreibt den übergebenen Text in ein Textinput / Textfeld.
  • element.submit() – Sendet ein Formular.
  • element.click() – Klickt ein Element, z.B. Checkbox, Button, usw.
  • element.switchTo() – Liefert ein TargetLocator, mit dem man in ein Window/ Frame/ Popup wechseln kann.

Nächster Schritt: Warten

Da die verschiedenen Ressourcen einer Webseite nie sofort verfügbar sind, kann (und sollte) man den WebDriver warten lassen, bevor man mit der aufgerufenen Seite arbeitet.

Dafür gibt es zwei verschiedene Möglichkeiten: implizit und explizit. Man sollte sich für eine der beiden Methoden entscheiden, da ein Mischen unvorhergesehenes Verhalten verursachen kann.

Implizites Warten

Ein implizites Warten bewirkt, dass der WebDriver bei der Suche nach einem Element den DOM so lange abfragt, bis das Element vorhanden ist, oder der eingestellte Timeout eintritt. Wird das implizite Timeout einmal gesetzt, kann es nicht mehr verändert werden.

In folgendem Beispiel wird das implizite Timeout auf 30 Sekunden gesetzt, /cats auf example.com aufgerufen und das Element mit der ID catPic_1 abgerufen. Der WebDriver wartet jetzt bis zu 30 Sekunden, bevor er eine TimeoutException auswirft.

WebDriver driver = new HtlmUnitDriver();
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
driver.get("http://www.example.com/cats");
WebElement catPictureElement = driver.findElement(By.id("catPic_1"));

Explizites Warten

Im Gegensatz zum impliziten Warten definiert man beim explizierten Warten einen Zeitraum, in dem man auf das Eintreten einer bestimmten Bedingung, einer ExpectedCondition, wartet. Läuft der Zeitraum aus und die Bedingung wurde nicht erfüllt, wird eine TimeoutException geworfen. Es gibt verschiedene ExpectedConditions. Die meisten beziehen sich auf Sichtbarkeit oder Interagierbarkeit eines Elements. Zusätzlich lassen sich mehrere Bedingungen via or oder and verknüpfen.

Im folgenden Beispiel wird ein Timeout von 30 Sekunden definiert und gewartet, bis dieses abgelaufen oder die Bedingung, nämlich das Vorhandensein des Elements mit der id „catPic_1“, erfüllt ist:

WebDriver driver = new HtmlUnitDriver();
driver.get("http:///www.example.com/cats");
int timeout = 30;
WebDriverWait wait = new WebDriverWait(driver, timeout);
WebElement catPictureElement = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.id("catPic_1"))
);

3. Testen mit Selenium WebDriver

Da wir nun die grundlegenden Funktionen des WebDrivers kennen, können wir unsere ersten Tests schreiben. Als Webseite dient folgende simple Seite:

<html>
    <head>
        <title>Quiz</title>
        <script>
            function checkresult() {
                var result = document.getElementById("response").value;
                var alertText = "Falsch!";
                if (result == 4) {
                    alertText = "Richtig!";
                }
                alert(alertText);
            }
        </script>
    </head>
<body>
    <div id="content">
        <h1>Quiz</h1>
        <p id="question">Was ergibt 2 + 2?</p>
        <form onSubmit="checkresult()">
            <input id="response" type="number">
            <input type="submit" value="Antworten">
        </form>
   </div>
</body>
</html>

Es kann jetzt Verschiedenes getestet werden, z.B. statische Inhalte wie der Titel der Seite oder die Formulierung der Frage. Tests statischer Inhalte sind in der Regel wenig komplex. Nicht immer ist es sinnvoll, diese zu automatisieren, da sie teilweise händisch schneller und einfacher von statten gehen.

Ein erster statischer Test, der die Formulierung der Frage überprüft, könnte wie folgt aussehen:

import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import static org.junit.Assert.assertEquals;
public class QuizTest {
//Pfad oder URL zur Webseite
private final String URL = "file:///simpleQuiz/simpleQuiz.html";
@Test
public void testQuestionIsCorrect() throws Exception {
    //Neuen WebDriver erzeugen
    WebDriver driver = new FirefoxDriver();
    //Zu unserer Webseite navigieren
    driver.get(URL);
    //Warten bis die Seite so weit geladen hat, dass der Titel "Quiz" vorhanden ist
    //oder 10 Sekunden verstrichen sind.
     (new WebDriverWait(driver, 10)).until(
         (ExpectedCondition<Boolean>) (WebDriver d) -> d.getTitle().contains("Quiz")
    );
    //Das Element holen, welches unsere Frage enthält
    WebElement question = driver.findElement(By.id("question"));
    //Prüfen, ob der sichtbare Text der Frage unserer Erwartung entspricht
    assertEquals("Was ergibt 2 + 2?", question.getText());
    //Den Browser beenden
    driver.quit();
    }
}

Komplexer sind in der Regel Funktionstests. Diese überprüfen die Webseite auf korrekte Funktionalitäten. In unserem Fall wäre dies z.B. eine Überprüfung, ob bei Eingabe einer richtigen oder falschen Antwort das korrekte Popup erscheint.

Um dies zu testen, wird unsere oben genannte Testklasse um folgenden Testfall erweitert:

@Test
public void testCorrectAnswer() {
    //Neuen WebDriver erzeugen
    WebDriver driver = new FirefoxDriver();
    //Zu unserer Webseite navigieren
    driver.get(URL);
    //Warten bis die Seite so weit geladen hat, dass der Titel "Quiz" vorhanden ist 
    //oder 10 Sekunden verstrichen sind.
     (new WebDriverWait(driver, 10)).until(
         (ExpectedCondition<Boolean>) (WebDriver d) -> d.getTitle().contains("Quiz")
    );
    //Das Input Feld für die Antwort holen
    WebElement responseInput = driver.findElement(By.id("response"));
    //Die korrekte Antwort eintragen
    responseInput.sendKeys("4");
    //Nun das Form submitten, der WebDriver findet das Form welches das
    //Inputfeld enthält automatisch
    responseInput.submit();
    //Jetzt sollte sich ein Alert Fenster geöffnet haben in das wir wechseln müssen
    Alert alert = driver.switchTo().alert();
    //Prüfen ob die Antwort als richtig erkannt wurde
    assertEquals("Richtig!", alert.getText());
    //Den Browser beenden
    driver.quit();
}

Dies sollte dann auch für den Negativfall durchgeführt werden. Für kleinere Seiten reicht diese Vorgehensweise durchaus. Ist das zu Automatisierende allerdings umfangreicher, so lohnt es sich, mehr Überlegung in den Aufbau/die Strukturierung der Tests zu investieren.

Ein beliebter Ansatz ist das Page Object Design Pattern.

4. Page Object Design Pattern

Dieses Pattern ist ein objektorientierter Ansatz, um die Tests übersichtlich, erweiterbar und wartbar zu gestalten. Für jede zu testende Seite wird ein sogenanntes Page Object erstellt, das die Funktionalitäten der Seite kapselt. Diese Methoden werden danach von den Tests bedient.

Unser kleines Quiz würde als Page Object dann so aussehen:

import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
public class QuizPage {
    private final WebDriver DRIVER;
    private final String EXPECTED_TITLE = "Quiz";
    public QuizPage(WebDriver driver) {
        this.DRIVER = driver;
        //Man kann (und sollte) überprüfen, ob sich der WebDriver auch wirklich
        //auf der erwarteten Seite befindet (in unserem Fall title = Quiz)
        if (!driver.getTitle().equals(EXPECTED_TITLE)) {
                throw new IllegalStateException(
                        "Expected " + EXPECTED_TITLE + " but got " + driver.getTitle() + " !"
                );
        }
    }

    public String getTitle() {
        return EXPECTED_TITLE;
    }
    //Schnittstelle zum Abfragen des Textes der Frage
    public String getQuestion() {
        //Das Element holen, welches unsere Frage enthält
        WebElement question = DRIVER.findElement(By.id("question"));
        return question.getText();
    }
    //Hier definieren wir eine Schnittstelle mit der wir später aus den Tests
    //heraus eine Antwort eingeben und abschicken können
    public QuizPage submitAnswer(String answer) {
        //Das Input Feld für die Antwort holen
        WebElement responseInput = DRIVER.findElement(By.id("response"));
        //Die korrekte Antwort eintragen
        responseInput.sendKeys(answer);
        //Nun das Form submitten, der WebDriver findet das Form welches das
        //Inputfeld enthält automatisch
        responseInput.submit();
        //Da wir die Seite nicht verlassen haben geben wir diese zurück
        return this;
    }
    //Mit dieser Methode geben wir den Text des PopUps zurück, wenn nicht
    //vorhanden wird eine wird .alert() eine Exception werfen
    public String getAlertText() {
        //Erstmal in den PopUp zu wechseln. Wenn dieser nicht vorhanden ist wirft
        //der WebDriver eine NoAlertPresentException
        Alert alert = DRIVER.switchTo().alert();
        //Nun geben wir den Text zurück
        return alert.getText();
    }
}

Einmal umgeschrieben, erscheint unsere Testklasse dann wie folgt:

import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
public class PageObjectQuizTest {
    private final String URL = "file:///simpleQuiz/simpleQuiz.html";
    private WebDriver driver;
    //Das Erstellen des WebDrivers sowie das Navigieren zur URL haben wir in den
    //Before Block ausgelagert
    @Before
    public void setUp() {
        //Neuen FirefoxDriver erzeugen
        driver = new FirefoxDriver();
        //Zu unserer Webseite navigieren
        driver.get(URL);
    }
    //Das Schließen des WebDrivers haben wir in den After Bock ausgelagert
    @After
    public void tearDown() {
        driver.quit();
    }
    @Test
    public void testQuestionIsCorrect() throws Exception {
        //Erstellen der neuen QuizPage
        QuizPage QuizPage = new QuizPage(driver);
        //Prüfen, ob der sichtbare Text der Frage unserer Erwartung entspricht
        assertEquals("Was ergibt 2 + 2?", QuizPage.getQuestion());
    }
    @Test
    public void testCorrectAnswer() {
        //Erstellen der neuen QuizPage
        QuizPage page = new QuizPage(driver);
        //Richtige Antwort eingeben, abschicken und PupUp Text holen
        String alertText = page.submitAnswer("4").getAlertText();
        //Prüfen ob die Antwort als richtig erkannt wurde
        assertEquals("Richtig!", alertText);
    }
    @Test
    public void testIncorrectAnswer() {
        //Erstellen der neuen QuizPage
        QuizPage page = new QuizPage(driver);
        //Falsche Antwort eingeben, abschicken und PupUp Text holen
        String alertText = page.submitAnswer("3").getAlertText();
        //Prüfen ob die Antwort als richtig erkannt wurde
        assertEquals("Falsch!", alertText);
    }
}

Das vorliegende Beispiel sollte veranschaulichen, wie Selenium sinnvoll für Webseiten-Tests eingesetzt werden kann. Diese Tests lassen sich auch automatisiert im Rahmen eines Deployment-Prozesses bei Continuous Integration einsetzen.

Nützliche Ressourcen zur weiteren Vertiefung können in der offiziellen Doku abgerufen werden.

(Luca Giorgi)

In unserer Reihe synTechTalk geben unsere DEV und OPS Teams Einblicke in ihr Technologie- und Architektur- KnowHow zur Umsetzung erfolgreicher digitaler Geschäftsmodelle.

Keine Kommentare