Suche
Teil 9: Backend meets Frontend: Testumgebung für verschiedene Browserversionen

Parallele UI-Tests mit Selenium Grid und Docker

Sven Ruppert

© Shutterstock / HelenField

Eine Web-UI kann Entwickler recht einfach in den Wahnsinn treiben. Dann nämlich, wenn Bugs nur bei bestimmten Betriebssystem/Browser-Kombinationen auftreten. Ein mit Docker aufgebaute Testumgebung in Selenium Grid kann diese Tests parallel abarbeiten.

Bisher haben wir unsere Vaadin-Web-App mit Selenium und TestBench getestet. Dazu haben wir den
Servlet-Container gestartet und mit Selenium und TestBench lokal einen Browser gestartet, mit dem die UI-Tests durchgeführt worden sind.

Bisher ist das auch ausreichend gewesen. Spätestens wenn man sich jedoch mit speziellen Browserversionen auseinandersetzen muss, da es einen Bug in einer bestimmtem Betriebssystem/Browserversions-Kombination gibt, denk man über Alternativen nach. Ebenfalls ist zu bedenken, die Testabläufen zu beschleunigen. Nicht die Ablaufgeschwindigkeit von einem Test an sich steht hier im Mittelpunkt, eher die Möglichkeit die Gesamtdauer aller Tests zu verringern. Kurz vorweg gesagt: Das ist vom Grundsatz her nicht so schwer, da man alle Kombinationen parallel testen kann. Naja, ein paar Kleinigkeiten sind sind leider doch vorher noch zu klären.

Selenium Grid als Testumgebung

Unter Selenium Grid  versteht man eine Umgebung, in der ein Verbund aus virtuellen oder physikalischen Rechnern Browserinstanzen zur Verfügung stellt, mit denen eine Web-App getestet werden kann. Die Menge der Rechner kann auch aus einem einzelnen Knoten bestehen. Das bedeutet, dass man auch einen einzelnen Knoten auf dem eigenen Rechner direkt installieren, starten und damit für die Durchführung von Tests verwenden kann.

Beginnen wir damit, eine Ablaufumgebung zu erstellen, in der wir ein Selenium Grid aufbauen können. Am einfachsten ist das mit Docker. Docker gibt es mittlerweile für alle gängigen Plattformen. Dazu zähle ich Linux, z. B. Debian, Windows und MacOS. Eine Übersicht über alle unterstützten Platformen gibt es hier.

Nachdem die Installation durchgeführt worden ist, kann man mit der Verwendung von Docker beginnen. Starten wir hierzu ein Terminal auf dem Rechner, auf dem Docker installiert ist. Auf der Kommandozeile sind die nachfolgenden Befehle auszuführen:

docker run -d -p 4444:4444 --name selenium-hub selenium/hub:latest
docker run -d --name selenium-node-chrome --link selenium-hub:hub selenium/node

Mit diesen Kommandos werden alle Teile geholt, die wir für einen Test mit Google Chrome benötigen:
Als erstes die Steuerung des Selenium Grid, nachfolgend einen Knoten, der uns einen Google Chrome
bereitstellt. Das hier verwendete Betriebssystem ist Linux. Um zu testen ob auch alles soweit
funktioniert, kann man unter der Annahme, dass der Docker-Dienst auf der eigenen Maschine läuft, mit der URL http://localhost:4444/grid/console nachsehen.

Hier sieht man, welche Version von Selenium verwendet worden ist, unter welchem Betriebssystem der Knoten läuft (hier Linux), die IP und der Port und auch welcher Browser in welcher Version zum Einsatz kommt (Abb. 1).

Abb. 1: Man sieht, welche Version von Selenium verwendet worden ist, unter welchem Betriebssystem der Knoten läuft, die IP und der Port und auch welcher Browser in welcher Version zum Einsatz kommt

Wenn man nun zum Tab Configuration wechselt, bekommt man noch weitere Informationen, die man auch später zur Konfiguration der WebDriver verwenden kann (Abb. 2).

Abb. 2: Im Tab Configuration bekommt man noch weitere Informationen, die man später zur Konfiguration der WebDriver verwenden kann

Um sich die Logfiles des Selenium Grid anzusehen, kann man in einem Terminal das folgende Docker-Kommando eingeben:

docker logs -f selenium-hub

Nur der Vollständigkeit halber hier noch die Kommandos, um diese Instanzen zu stoppen und
nachfolgend wieder zu starten.

docker stop selenium-node-chrome
docker stop selenium-hub

docker start selenium-hub
docker start selenium-node-chrome

Das Löschen aller Komponenten ist mit dem Kommando rm realisiert.

docker image rm --force selenium/node-chrome
docker image rm --force selenium/hub

Nun haben wir alles zusammen, um eine in Docker laufende Version eines Selenium Grid zu bekommen, laufen zu lassen und bei Bedarf wieder zu löschen. Da dieses Verfahren so einfach ist, kann ich nur empfehlen sie einer klassischen lokalen Installation vorzuziehen. Wie wir mit anderen Betriebssystemen umgehen, werden wir uns zu in einem weiteren Teil gesondert ansehen.

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 im Selenium Grid

Nun haben wir eine lauffähige Installation. Aber wie lassen wir nun unsere Tests damit laufen? Wir müssen eine Docker-spezifische Frage vorab klären. Unter welcher IP ist von einem Selenium-Grid-Knoten aus unsere Vaadin-Web-App zu erreichen? Innerhalb von Docker haben die Knoten andere IPs. in unserem Fall ist der Google-Chrome-Knoten mit der IP http://172.17.0.3:5555 versehen. Welche URL muss nun dieser Knoten aufrufen, um auf die Web-App zu gelangen, die von unserem jUnit Runner gestartet worden ist? Die Antwort ist in diesem Fall recht einfach. Wir nehmen einfach unsere eigene IP, die nicht localhost ist.

Allerdings möchten wir den Test ja nicht jedesmal umschreiben, nur weil wir von unserem DHCP eine neue IP zugewiesen bekommen haben oder der Test auf dem Rechner von unserem Arbeitskollegen läuft. Um die IP zur Laufzeit herauszufinden, kann man das eigene System abfragen und alle hierfür nicht gültigen IPs herausfiltern. Hierfür habe ich in dem Interface WebDriverFunctions die Funktion ipSupplierLocalIP definiert und nachfolgend aufgelistet.

Supplier<String> ipSupplierLocalIP = () -> {
   final CheckedSupplier<Enumeration>NetworkInterface>> checkedSupplier
      = NetworkInterface::getNetworkInterfaces;
   return Transformations<NetworkInterface>enumToStream()
      .apply(checkedSupplier.getOrElse(Collections::emptyEnumeration))
      .map(NetworkInterface::getInetAddresses)
      .flatMap(iaEnum -> Transformations<InetAddress>enumToStream().apply(iaEnum
      .filter(inetAddress -> inetAddress instanceof Inet4Address)
      .filter(not(InetAddress::isMulticastAddress))
      .map(InetAddress::getHostAddress)
      .filter(notEmpty())
      .filter(adr -> notStartsWith().apply(adr, "127"))
      .filter(adr -> notStartsWith().apply(adr, "169.254"))
      .filter(adr -> notStartsWith().apply(adr, "255.255.255.255"))
      .filter(adr -> notStartsWith().apply(adr, "255.255.255.255"))
      .filter(adr -> notStartsWith().apply(adr, "0.0.0.0"))
      // .filter(adr -> range(224, 240).noneMatch(nr -> adr.startsWith(.findFirst().orElse("localhost");
};

Nun sind wir in der Lage, zur Laufzeit eine unserer IPs zu erlangen. Ich habe der Einfachheit halber
angenommen, das die erste gefundene IP ausreichend ist. Diese IP können wir dann anstelle von
localhost dem System zur Verfügung stellen. Hierfür setzen wir in der Methode beforeAll in unserer Klasse WebDriverSeleniumExtension das Property mit dem Schlüssel KEY_VAADIN_SERVER_IP.

System.setProperty(KEY_VAADIN_SERVER_IP, ipSupplierLocalIP.get());

Zur Erinnerung: Diese Konstante ist im Interface VaadinPageObject definiert und wird verwendet, um den Basis-URL für unsere jUnit-Tests zu setzen.

String DEFAULT_PROTOCOL = "http";
String DEFAULT_IP = "127.0.0.1";
String DEFAULT_PORT = "8080";
String DEFAULT_WEBAPP = "";

String KEY_VAADIN_SERVER_PROTOCOL = "vaadin.server.protocol";
String KEY_VAADIN_SERVER_IP = "vaadin.server.ip";
String KEY_VAADIN_SERVER_PORT = "vaadin.server.port";
String KEY_VAADIN_SERVER_WEBAPP = "vaadin.server.webapp";

default BiFunction<String, String, String> property() {
   return (key, defaultValue) -> (String) getProperties().getOrDefault(key, defaultValue
}

default Supplier protocol() {
return () -> property().apply(KEY_VAADIN_SERVER_PROTOCOL, DEFAULT_PROTOCOL)
}

default Supplier ip() {
   return () -> property().apply(KEY_VAADIN_SERVER_IP, DEFAULT_IP);
}

default Supplier port() {
   return () -> property().apply(KEY_VAADIN_SERVER_PORT, DEFAULT_PORT);
}

default Supplier webapp() {
   return () -> property().apply(KEY_VAADIN_SERVER_WEBAPP, DEFAULT_WEBAPP);
}

default Supplier baseURL() {
   return () -> protocol().get() + "://" + ip().get() + ":" + port().get() + "/"
}

default Supplier url() {
   return () -> baseURL().get() + webapp().get();
}

Wenn wir den Test nun laufen lassen, verhält er sich exakt wie vorher. Das ist auch nicht weiter verwunderlich, da wir ja lediglich die IP ausgewechselt haben, aber auf das selbe Ziel zeigen. Was noch fehlt, ist die Verwendung der richtigen Implementierung für das Interface WebDriver. In dem Interface WebDriverFunctions befinden sich die Methoden zur Erzeugung der Funktionen, die wir zur Initialisierung der WebDriver-Instanzen verwenden.

Nehmen wir die Methode newWebDriverChrome als Vorlage:

static Supplier<optional<WebDriver>> newWebDriverChrome() {
   return () -> {
      try {
         final DesiredCapabilities chromeCapabilities = DesiredCapabilities.chrome
         final ChromeDriver chromeDriver = new ChromeDriver(chromeCapabilities);
         chromeDriver.manage().window().maximize();
         return Optional.of(chromeDriver);
      } catch (Exception e) {
         e.printStackTrace();
         return empty();
      }
   };
}

Dann müssen wir zwei Dinge ändern. Zum einen die Implementierung, die wir verwenden. In diesem Fall wird es eine Instanz der Klasse RemoteWebDriver. Und zum anderen den URL, den wir zur Initialisierung verwenden. Wir zeigen nun auf den Hauptknoten von unserem Selenium Grid.

final URL url = new URL("http://" + "127.0.0.1" + ":4444/wd/hub");

In unserem Fall ist es die IP 127.0.0.1. Alles zusammen ergibt dann die nachfolgende Implementierung.

static Supplier<optional<<WebDriver>> newWebDriverChromeRemote() {
   return () -> {
      try {
         final DesiredCapabilities chromeCapabilities = DesiredCapabilities.chrome
         final URL url = new URL("http://" + "127.0.0.1" + ":4444/wd/hub");
         final RemoteWebDriver webDriver = new RemoteWebDriver(url, chromeCapabilities
         webDriver.manage().window().maximize();
         return Optional.of(webDriver);
      } catch (Exception e) {
         e.printStackTrace();
         return empty();
      }
   };
}

Diesen RemoteWebDriver müssen wir nun noch verwenden, was in der Klasse PageObjectProvider passiert.

   public static class PageObjectProvider implements ArgumentsProvider {
      @Override
   public Stream<? extends Arguments> arguments(ContainerExtensionContext context
      return Stream
         .of(
            newWebDriverChromeRemote()//,
// newWebDriverChrome()//,
// newWebDriverFirefox()
         )
         .map(Supplier::get)
         .filter(Optional::isPresent)
         .map(Optional::get)
         .peek(d -> storeWebDriver().accept(context, d))
         .map(MyUIPageObject::new)
         .map(ObjectArrayArguments::create);
      }
   }

Zur Erinnerung: Diese Klasse verwenden wir, um die Eingangsparameter für unseren Test zu generieren. Hier werden die Instanzen für die Klasse MyUIPageObject erzeugt, auf denen der Test dann durchgeführt wird.

Fazit

Wir sind nun in der Lage, unsere Tests in ein Selenium Grid auszulagern. In unserem Fall haben wir dieses auf dem eigenen Rechner innerhalb von Docker realisiert. Genauso kann man das auch in ein Netzwerk oder ins Internet auslagern. Somit können wir nun unabhängig von der
Entwicklungsplattform auf Linux und in den dort angebotenen Selenium Grid Nodes für Google Chrome, Firefox unf PhantomJS testen. Bisher ist der Lebenszyklus noch unabhängig von einem Build-Prozess. Wir gehen also davon aus, dass unser Selenium Grid zum Testzeitpunkt voll funktionsfähig und erreichbar ist. In einem weiteren Teil werden wir uns ansehen wie man das kombiniert kann und wie wir weitere Plattformen wie Windows in dieses Konzept einbinden können.

Den Quelltext findet ihr auf GitHub. Bei Fragen und 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.