Kolumne: Backend meets Frontend Teil 3: Selenium Testbench

Testgetriebene Entwicklung mit Vaadin und Selenium

Sven Ruppert

© Shutterstock / HelenField

Die oft fehlende technische Unterstützung ist eine gern gewählte Ausrede, um UIs nicht testen zu müssen. Dass diese Ausrede aber nicht mehr zählt, zeigt der aktuelle Teil der Kolumne „Backend meets Frontend“, wie Entwickler mit Vaadin und Selenium die Grundtseine für eine testgetriebene Entwicklung legen können.

Testgetriebene Entwicklung ist auf der Backend-Seite sehr verbreitet und hat schon viele verschiedene Variationen herausgebracht. Allerdings muss ich leider immer wieder sehen, dass es auf der UI-Seite leider nicht ganz so gut aussieht. Das hat sicherlich auch manchmal technische Gründe, dennoch ist das keine Ausrede.

Im letzten Teil haben wir gesehen, wie man Selenium für ein Projekt so einrichtet, dass man in der Lage ist, einen WebDriver zu erzeugen und mit einem Browser lokal zu interagieren. Das einfache Aufrufen einer Seite wie im letzten Teil ist natürlich nicht ausreichend. In diesem Teil werden wir uns ansehen, wie die ersten, zugegebenermaßen noch recht einfachen, Tests für ein UI geschrieben werden können.

Die erste Erweiterung der UI

Abb. 1: Das UI bekommt zwei Textfelder und einen Knopf dazu

Da wir nun eine lauffähige Installation von Selenium haben und der Test die Initialisierungsarbeiten erledigt, können wir mit dem ersten fachlich/logischen Test beginnen. Dazu erweitern wir das UI um zwei Textfelder und einen Button. Ziel ist es, das in dem linken Textfeld eine Eingabe erfolgt, die dann durch das Drücken auf den Button in das rechte Textfeld kopiert wird. Das rechte Textfeld soll ReadOnly sein. Um das nun in Vaadin zu realisieren, können wir eins zu eins vorgehen.

public class MyUI extends UI {

  public static final String BUTTON_ID = "buttonID";
  public static final String INPUT_ID = "inputID";
  public static final String OUTPUT_ID = "outputID";

  @Override
  protected void init(VaadinRequest request) {
    final HorizontalLayout layout = new HorizontalLayout();

    final TextField input = new TextField();
    input.setId(INPUT_ID);

    final Button button = new Button("click me");
    button.setId(BUTTON_ID);

    final TextField output = new TextField();
    output.setReadOnly(true);
    output.setId(OUTPUT_ID);    

    layout.addComponents(input,button,output);

    button.addClickListener(
        event -> output.setValue(input.getValue()));


    setContent(layout);
  }
}

Abb. 2: Das Ergebnis der ersten UI-Erweiterung

Hier gibt es noch ein paar Dinge, die ich gerne hervorheben möchte. Die IDs der Komponenten sind als Konstante in der Klasse MyUI definiert. Das macht es möglich, diese später in den Tests zu referenzieren, ohne das es zu Flüchtigkeitsfehlern kommt oder Refactoring uns diesbezüglich ärgern wird.

Nun gibt es hier verschiedene Ansätze in welcher Art und Weise die Definition und Deklaration der Logikanteile erfolgen soll. Aus meiner Sicht allerdings ist es sinnvoll, den Aufbau des UI von der Assoziation der UI-Logik zu trennen. Demnach definiere ich die Funktion des Buttons als letztes.

Backend meets Frontend

In der Artikelserie Backend meets Frontend stellt Sven Ruppert (Vaadin) Konzepte und Technologien rund um das UI-Framework Vaadin vor. Sein Fokus liegt dabei auf modernem Web-Design für Java-Backend-Entwickler.

Zum ersten Teil und damit dem Start der Tutorien rund um die UI-Entwicklung mit Java geht es hier entlang. Alle Teile der Serie Backend meets Frontend finden sich hier.

Der erste Test: Klick den Button

Im UI ist nun ein Button vorhanden, den es zu finden und zu aktivieren gilt. Nachdem mittels driver.get("http://127.0.0.1:8080/"); der WebDriver den URL geholt hat, kann man in der Struktur nach Elementen suchen. In diesem Fall nach dem Button mit der ID, die wir ihm in dem UI gegeben haben: final WebElement button = driver.findElement(By.id(MyUI.BUTTON_ID));. Um dies nicht immer wieder schreiben zu müssen, definieren wir uns drei Helper-Methoden.

  private WebElement button(WebDriver driver) {
    return driver.findElement(By.id(MyUI.BUTTON_ID));
  }

  private WebElement output(WebDriver driver) {
    return driver.findElement(By.id(MyUI.OUTPUT_ID));
  }

  private WebElement input(WebDriver driver) {
    return driver.findElement(By.id(MyUI.INPUT_ID));
  }

Diese holen mittels der ID die jeweilige Komponente. Wir erhalten mittels button(driver) eine Instanz der Klasse WebElement, auf der wir verschiedene Aktionen ausführen können. Leider ist es keine Button-Instanz, sondern eine generische WebElement-Struktur. Wenn wir nun prüfen wollen, ob die Beschriftung von dem Element die ist, die wir erwarten, können wir den Inhalt mit der Methode getText() erhalten und vergleichen.

    Assert.assertNotNull(button(driver));
    Assert.assertEquals("click me",
                        button(driver).getText());

Wenn das stimmt, prüfen wir als nächstes, ob wir den Button klicken können. Hierzu müssen wir einen Text in das linke Textfeld eintragen und dann den Button drücken, um den Text aus dem rechten Textfeld zu holen. Ein Assert stellt nun fest, ob der Text auf der rechten und linken Seite gleich ist.

    Assert.assertEquals("", output(driver).getText());

    input(driver).sendKeys("Hello World");

    final WebElement button = button(driver);

    Assert.assertNotNull(button);
    String text = button.getText();
    Assert.assertEquals("click me", text);

    button.sendKeys(Keys.ENTER);

    String outputText = output(driver).getAttribute("value");
    System.out.println("outputText = " + outputText);

    Assert.assertEquals("Hello World", outputText); 

Wir sind nun soweit, dass ein Test (auf meiner Maschine) einen Test in unter drei Sekunden durchführt und dabei

  • die JVM startet
  • den Servlet-Container startet
  • die WebApp deployed
  • den lokalen Browser Chrome startet
  • die Verbindung mittels Selenium herstellt
  • die logischen Tests durchführt
  • den WebDriver schließt
  • den Browser beendet
  • den Servlet-Conatiner stoppt

Abb. 3: Die resultierende Vererbungsstrategie

Da nun der Selenium-Test läuft, können wir die technischen Dinge von Selenium in die Klasse BaseSeleniumTest auslagern. Dazu zählt das setzen der System-Properties für die Lokation der Webdriver Binaries, das Erzeugen des WebDrivers und der Lebenszyklus der eingehalten werden soll. Damit ergibt sich dann die Vererbungshierarchie aus Abbildung 3.

public class BaseSeleniumTest extends BaseTest {

  protected Optional<WebDriver> driver;

  @Override
  @Before
  public void setUp() throws Exception {
    super.setUp();

    // init webDriver here
    System.setProperty("webdriver.chrome.driver", "_data/chromedriver");

    driver = Optional.of(new ChromeDriver());
    //final WebDriverWait wait = new WebDriverWait(driver, 10);
  }

  @Override
  @After
  public void tearDown() throws Exception {
    // kill webdriver / Browser here
    driver.ifPresent(d -> {
      d.close();
      d.quit();
    });
    driver = Optional.empty();
    super.tearDown();
  }

  protected WebElement button(WebDriver driver) {
    return driver.findElement(By.id(MyUI.BUTTON_ID));
  }

  protected WebElement output(WebDriver driver) {
    return driver.findElement(By.id(MyUI.OUTPUT_ID));
  }

  protected WebElement input(WebDriver driver) {
    return driver.findElement(By.id(MyUI.INPUT_ID));
  }
}

Und die Testklasse sieht nun wie folgt aus:

public class MyUITest extends BaseSeleniumTest {
  @Test
  public void test001() throws Exception {

    if (! driver.isPresent()) Assert.fail("WebDriver not available");

    driver.ifPresent(d -> {
      d.get("http://127.0.0.1:8080/");

      Assert.assertNotNull(button(d));
      Assert.assertEquals("click me",
                          button(d).getText());

      Assert.assertEquals("", output(d).getText());

      input(d).sendKeys("Hello World");

      final WebElement button = button(d);

      Assert.assertNotNull(button);
      String text = button.getText();
      Assert.assertEquals("click me", text);

      button.sendKeys(Keys.ENTER);

      String outputText = output(d).getAttribute("value");
      Assert.assertEquals("Hello World", outputText);
    });
  }
}

Damit haben wir die technischen Notwendigkeiten schon recht gut ausgelagert und können diese dann in den nachfolgenden Tests wieder verwenden.

Fazit

Wir sind nun in der Lage automatische UI-Tests zu schreiben, die lokal auf der Entwicklermaschine laufen. Einer testgetriebenen Entwicklung über alle Teile der Anwendung steht nun nichts mehr im Wege. Allerdings gab es einige Kleinigkeiten zu beachten, wie die Installation der WebDriver Binaries.

In dem nächsten Teil werden wir uns ansehen, wie wir mit Selenium im Detail umgehen werden und mit welchen Herausforderungen man an der einen oder anderen Stelle umgehen muss.

Den Quelltext findet ihr auf GitHub. Bei Fragen oder Anregungen einfach melden unter
sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.