Herzlich willkommen auf dem Blog der exensio GmbH

exensio ist ein unabhängiges Software-Unternehmen von erfahrenen IT Beratern und Architekten. Zu unseren Kunden zählen namhafte deutsche Großunternehmen.

exensio verbessert das Kommunikations- und Informationsmanagement von Unternehmen. Auf Basis sinnvoller und zweckmäßiger Technologien erarbeiten wir flexible und übergreifende Lösungen für webbasierte Informationssysteme. Abhängig von den Kundenanforderungen realisieren wir Web Applikationen sowie Lösungen auf Basis von Web Content Management- und Portalsystemen. Integrativ, wirtschaftlich und anwenderfreundlich!

Hier gelangen Sie zur exensio GmbH.

Montag, 29. September 2014

Automatische Browsertests mit Selenium: Se Builder in Verbindung mit Selenium Grid (Teil 5)

Diese 6-teilige Blogpost-Reihe befasst sich mit dem Thema Test-Automatisierung für grafische Benutzeroberflächen von Webanwendungen. Im Wesentlichen kommt die Selenium Test Tool Suite zum Einsatz. Im Speziellen wird der Umgang mit dem Tool Se Builder erklärt, mit dem Tests aufgenommen und abgespielt werden können.

Der ersten Teil [1] gab eine kurze Einführung zum Tool Selenium Builder, die Installation sowie einen Vergleich des Tools mit der Selenium IDE. Im zweiten Teil [2] haben wir das Selenium JSON-Format untersucht und eine Test-Suite erstellt. Im dritten und vierten Teil [3][4] habe ich Einblicke in meine Arbeit gegeben und versucht, Fallstricke aufzuzeigen.

In diesem Teil zeige ich, wie man ein Selenium Grid einrichtet und wie man es mit dem Se Builder verwendet. Im letzten Teil wird es dann um automatisierte Tests zusammen mit dem CI Server Jenkins.

Warum Selenium Grid?

Mit Selenium Grid lassen sich Testläufe auf unterschiedlichen Maschinen mit verschiedenen Browsern parallel ausführen. Im Wesentlichen ermöglicht Selenium Grid die verteilte Ausführung von Testläufen. Es erlaubt die Ausführung der Testläufe in einer verteilten Testumgebung. Laut Dokumentation und Kommentare der Entwickler [5], sprechen die folgenden beiden Gründe für den Einsatz von Selenium Grid:
  • Bei Testfällen mit unterschiedlichen Browsern, unterschiedlichen Betriebssystemen und parallelen Testläufen
  • Um Zeit bei der Abarbeitung der Testläufe zu sparen
Obwohl Claretportal natürlich ein Multiuser-System ist und problemlos mehrere User-Interaktionen gleichzeitig bedienen kann, beschränken wir uns bei den Testläufen auf einzelne Testszenarien. Es kommen keine parallelen Testläufe zur Anwendung. Trotzdem haben wir uns für Selenium Grid entschieden, und zwar aus den folgenden Gründen:
  • Selenium Grid im Zusammenspiel mit dem CI Server Jenkins
    Das Grid kann sehr einfach über ein PlugIn des Continous Integration Servers Jenkins in unsere bestehende Systeme eingebunden werden.
  • Unterschiedliche Browser
    Wir müssen die Kompatibilität zum Internet Explorer 8 (IE8) sicherstellen. Gleichzeitig entwickeln wir aber unter Chrome. Und natürlich wollen wir die Unterstützung moderner Browser gewährleisten. Selenium Grid bietet dafür eine große Flexibilität.

Wie funktioniert Selenium Grid?

Ich beziehe mich hier nur auf die Version 2.x von Selenium Grid, oft auch als Grid 2 oder Grid 2.0 bezeichnet. Selenium Grid verwendet ein Hub-Node-Konzept. Der Hub weiß, welche Nodes mit welchen Eigenschaften ihm zur Verfügung stehen und steuert entsprechend alle Testläufe. Als Anwender interagiert man einzig mit dem Hub.
Den Nodes wird beim Start per Parameter oder besser per JSON Konfigurationsdatei mitgeteilt, welche Eigenschaften sie anbieten können. Beispielsweise kann man einen Node starten und ihm mitteilen, er dürfe eine IE8 Instanz anbieten. Der Node meldet sich dann bei seinem Hub mit dieser Information an. Sobald der Hub eine Anfrage für einen Testlauf unter IE8 erhält, prüft er, ob er einen Node mit dieser Eigenschaft kennt und ob der Node zur Zeit zur Verfügung steht (also angemeldet und frei ist). Treffen die Kriterien zu, wird der Testlauf vom Hub angenommen und an den entsprechenden Node weiterdelegiert. Dem Node werden dann alle benötigten Informationen weitergereicht [6].

Selenium Grid Hub Installation und Konfiguration

Ich möchte hier zwei mögliche Arten der Installation zeigen. Zum einen gibt es auf der Webseite des SeleniumHQ die Standalone Variante von Selenium Grid zum Download. Damit kann man sich sein eigenes Grid, bestehend aus Nodes und Hub, einrichten und betreiben. Zum anderen gibt es das Plugin für Jenkins. Damit wird das Selenium Grid, also Nodes und Hub, in Jenkins eingebunden. Außerdem zeige ich, wie man die Nodes und den Hub mittels einer JSON Konfigurationsdatei einrichtet.

Variante 1: Standalone
  1. Voraussetzung: Java ist installiert und Classpath entsprechend gesetzt
  2. Download des selenium-server-standalone-*.jar von [7].
    Aktuell wird dort die Version 2.42.2 zur Verfügung gestellt.
  3. Hub starten durch den Kommandozeilenaufruf
    java -jar selenium-server-standalone-2.*.jar -role hub
    Dabei wird der Hub größtenteils mit Standardwerten gestartet (Timeouts, Ports).

Variante 2: Mittels Jenkins
  1. Ggf. Jenkins aktualisieren
  2. Über Verwaltung | Plugins das Plugin mit der ID selenium [8] installieren
  3. Ggf. das Plugin selenium aktualisieren
  4. Über den Link http://<jenkinsurl>/selenium/configurations kann der Hub des Selenium Grids konfiguriert werden.
Durch Variante 2 erhält man sehr einfach und schnell einen funktionierenden Selenium Grid Hub. Durch den sehr guten und stabilen Update-Mechanismus von Jenkins und die große Aktivität bei der Entwicklung des Plugins ist man auf diese Weise immer auf dem neuesten Stand.

Selenium Grid Node Installation und Konfiguration

Die Verwendung und die Konfiguration eines Selenium Grid Nodes ist unabhängig von der Verwendung und der evtl. vorhandenen Konfiguration eines Jenkins Slaves. Ein Node muss manuell eingerichtet werden. Es besteht nicht die Möglichkeit, eine fertige Node-Konfiguration vom Jenkins-Server zu beziehen. Die Konfiguration und der Start eines Selenium Grid Nodes ist sehr einfach und funktioniert wie folgt:
  1. Voraussetzung: Java ist installiert und Classpath entsprechend gesetzt
  2. Download selenium-server-standalone-*.jar von http://docs.seleniumhq.org/download Aktuell wird dort die Version 2.42.2 zur Verfügung gestellt.
  3. Node starten durch den Kommandozeilenaufruf
    java -jar selenium-server-standalone-2.*.jar -role node \
    -hub http://<jenkinsurl>:4444/grid/register
    Dabei wird der Node größtenteils mit Standardwerten gestartet (Timeouts, Ports)
Wesentlich besser und komfortabler ist die Konfiguration per JSON Konfigurationsdatei, die wie folgt aussehen kann:
{
  "capabilities":
      [
        {
          "browserName": "firefox",
          "version": "29",
          "platform": "WINDOWS",
          "maxInstances": 3,
          "seleniumProtocol": "WebDriver",
          "binary": "c:\Program Files (x86)\Mozilla Firefox\firefox.exe"
        },
        {
          "browserName": "chrome",
          "version": "35",
          "platform": "WINDOWS",
          "maxInstances": 3,
          "seleniumProtocol": "WebDriver",
          "binary": "c:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
        },
        {
          "browserName": "iexplorer",
          "version": "11",
          "platform": "WINDOWS",
          "maxInstances": 1,
          "seleniumProtocol": "WebDriver"
        }
      ],
  "configuration":
  {
    "nodeTimeout":240,
    "nodePolling":2000,
    "maxSession": 3,
    "timeout":30000,
    "port": 5555,
    "host": ip,
    "register": true,
    "registerCycle": 5000,
    "cleanUpCycle":2000,
    "hubPort": 4444,
    "hubHost": 
  }
}

Hinweise:
  • Bei der Angabe von Versionsnummern (vor allem bei Chrome) bedenken, dass sich die Version durch den Auto-Update-Mechanismus des Browsers ändern kann. Also entweder die Versionsangabe gar nicht erst angeben oder die Auto-Update-Funktion deaktivieren.
  • Beim Matching des Betriebssystems (bzw. genauer der Plattform) wird empfohlen, ausschließlich Großbuchstaben zu verwenden um Missverständnisse zu vermeiden.
  • Browsernamen mit Leerzeichen (z. B. "internet explorer") vermeiden.
  • Die maximale Anzahl an Instanzen je Node sollte auf 5 oder 6 beschränkt sein. Eine größere Anzahl an Instanzen verlangsamt den Node zu sehr.
  • Als Protokoll verwende ich ausschließlich WebDriver. Stattdessen wäre aber auch Selenium möglich. Dann würde statt des WebDrivers das RC1 Protokoll von Selenium verwendet werden.
  • Natürlich muss "" durch den Servernamen oder die IP Adresse des Jenkins Servers ersetzt werden.
Entschließt man sich für die Verwendung einer JSON Konfigurationsdatei für den Node, dann sieht der Aufruf zum Start des Nodes wie folgt aus:
java -jar selenium-server-standalone-2.*.jar -role node -nodeConfig nodeconfig.json
Bei erfolgreicher Verbindung des Nodes mit dem Hub erhält man in Jenkins unter dem Link http://<jenkinsurl>/selenium/ eine gute Übersicht über die vorhandenen Nodes und deren Konfiguration.


Tipp: Autostart Selenium Grid

Für die Nodes verwenden wir VMs von modern.UI, die jeden morgen automatisch gestartet und abends heruntergefahren werden. Damit der Selenium Node automatisch gestartet wird, empfehle ich folgende Konfiguration:
  • Installation von TightVNC Server
  • Autologin eines beliebigen Users in dessen Kontext dann der Node gestartet wird
  • Batch-Skript im Autostart-Ordner des Users
  • Für IE und Chrome müssen die Treiber-Dateien IEDriverServer.exe und chromedriver.exe nach C:\Windows kopiert werden
Diese Konfiguration hat gegenüber z. B. der Konfiguration als Windows-Dienst den Vorteil, dass Screenshots am Node gemacht werden können. Denn ein Windows-Dienst hat kein grafisches Ausgabegerät. Screenshots resultieren in diesem Fall in einem schwarzen Bild. Mit der vorgeschlagenen Konfiguration können dagegen Testfälle erfolgreich auf dem Node abgespielt werden; inklusive der Erstellung von Screenshots. Dabei ist es nicht erforderlich, dass während des Abspielens ein User per VNC eingeloggt sein muss. Ist man dennoch eingeloggt, kann man die automatische Fernsteuerung des Browsers gut beobachten.

Testfall Remote ausführen

In den letzten Teilen dieser Blogpost Reihe haben wir bereits gelernt, wie man einen Testfall oder eine Test-Suite aufnimmt, ihn editiert und ihn später lokal abspielt. Der Vorteil im Grid besteht nun darin, dass Testfälle nicht mehr nur lokal abgespielt werden können, sondern auch remote auf einem anderen PC. Dabei kann man beim Absetzen des Jobs an den Hub angeben, mit welchen Eigenschaften der Job abgespielt werden soll.
In unserem Szenario steht neben unserem lokalen PC (der nicht Teil des Grids sein muss!) ein Node mit Firefox 29, Chrome 35 und IE8 zur Verfügung. Unser Testfall führt zu einer Webseite, die die User-Agent Angabe des Browsers auswertet. Das Skript sieht wie folgt aus:

{
  "type": "script",
  "seleniumVersion": "2",
  "formatVersion": 2,
  "steps": [
    {
      "type": "get",
      "url": "http://kluge.in-chemnitz.de/tools/browser.php"
    },
    {
      "type": "saveScreenshot",
      "file": "\\\\share\\myScreenshot.png"
    }
  ],
  "data": {
    "configs": {},
    "source": "none"
  },
  "inputs": [],
  "timeoutSeconds": 60
}

Das Skript öffnet die Webseite und macht einen Screenshot des Inhalts. Um den Testfall remote abzuspielen, öffne ich im Se Builder im Menü Run den Eintrag "Run on Selenium Server".


Daraufhin öffnet sich ein neuer Dialog mit dem Titel "Selenium Server Settings", in dem die IP-Adresse oder der Name des Selenium Grid Hub inkl. Port angegeben wird. Außerdem kann man dort spezifizieren, mit welchem Browser, welcher Version und auf welcher Plattform der Testlauf abgespielt werden soll. Diese Angaben sind optional und schränken die Auswahl verfügbarer Nodes ein.

Im ersten Schritt dieses Beispiels habe ich in dem neuen Dialog als Browser String den Wert "iexplorer" angegeben. Vergleiche dazu den Wert des Parameters browserName aus der JSON Konfigurationsdatei des Node, der ebenfalls "iexplorer" lautet. Außerdem habe ich als Browser Version "8" und als Platform "WINDOWS" angegeben. Nach Drücken des Knopfs "Run" ändert sich der Status rechts oben im Se Builder zu "connecting…". Gleich danach beginnt das Abspielen des Testfalls im Se Builder, parallel dazu ändert sich die Statusanzeige im Selenium Grid Hub (in unserem Fall in Jenkins -> Selenium Grid -> Status). Die freien und verfügbaren Browser-Instanzen werden dann wie folgt angegeben:

chrome 3/3, firefox 3/3, iexplorer 0/1
 
Die einzige verfügbare Internet Explorer Instanz auf diesem Node ist also derzeit belegt.
Öffnet man z. B. mittels TightVNC Viewer den Desktop des entfernten Rechners, so kann man dort das Browserfenster beobachten, wie es sich scheinbar auf magische Art und Weise von alleine öffnet und die im Testfall gespeicherten Schritte abgespielt werden. Für erfolgreiches Abspielen des Testfalls auf dem entfernten Rechner ist es nicht erforderlich, dass der Desktop mittels VNC geöffnet wird.
Ich habe den Testfall zwei Mal abgespielt und dabei den Browser String (und die Browser Version) geändert von iexplorer 8 zu firefox 29. Die Auswertung der User-Agents sieht wie folgt aus:



Bei den Selenium Tests wird also der tatsächliche User-Agent, also auch der tatsächliche Browser verwendet. Anders war es auch nicht zu erwarten, denn es wird ja mit dem Selenium WebDriver kein Browser simuliert, sondern tatsächlicher der definierte Browser verwendet.

Tipp: Unterschiedliche Browser verwenden

Bei der Verwendung von unterschiedlichen, entfernten Browsern sind folgende Dinge zu beachten:
  • Browser sollten im selben User Context gestartet werden wie der Selenium Grid Node Task.
  • Jeder Browser sollte mindestens ein Mal manuell im entsprechenden User Context gestartet werden, um die typischen Einstellungsabfragen zu deaktivieren (Standardbrowser, Seite übersetzen, etc.).
  • Automatische Update-Funktionen der Browser sollten deaktiviert werden.
  • Identische Standard-Sprache der Browser einstellen.
  • Firefox: zu verwendendes Profil definieren [10].
  • Internet Explorer: Identische Sicherheitsstufen für alle vier Zonen.
  • Identische Startseite oder keine Startseite für alle Browser.
Im letzten Teil dieser Reihe demonstriere ich ein Szenario mit Selenium Grid und dem Continous Integration Server Jenkins, mit dessen Hilfe Testläufe automatisch gestartet werden und der das Reporting übernimmt. Außerdem zeige ich, wie sich damit Testreihen realisieren lassen.

Links

[1] http://blog.exensio.de/2014/07/automatische-browsertests-mit-selenium.html
[2] href="http://blog.exensio.de/2014/08/automatische-browsertests-mit-selenium.html
[3] http://blog.exensio.de/2014/08/automatische-browsertests-mit-selenium_21.html
[4] http://blog.exensio.de/2014/09/automatische-tests-des-user-interface.html
[5] http://docs.seleniumhq.org/docs/07_selenium_grid.jsp
[6] http://guru99.com/introduction-to-selenium-grid.html
[7] http://docs.seleniumhq.org/download
[8] https://wiki.jenkins-ci.org/display/JENKINS/Selenium+Plugin
[10] https://code.google.com/p/selenium/wiki/TipsAndTricks

Mittwoch, 17. September 2014

LiMux - Software-Streit im Münchner Rathaus - eine Frage der Einstellung

Quelle: http://www.muenchen.de/
So titelte die Süddeutsche in einen Kommentar [1] über die vom frisch gewählten Münchner Oberbürgermeister Dieter Reiter losgetretene Diskussion, ob nicht doch von Linux auf Microsoft zurückgeschwenkt werden soll.

Vor ca. 10 Jahren wurde vom Münchner Stadtrat beschlossen, von Windows auf Linux zu migrieren. Der Gründe hierfür waren zum einen Kosteneinsparungen - aktuell geht man von 10 Mio Euro Einsparung aus - und eine Vorreiterrolle für offene Software zu zeigen.

Laut der offiziellen Homepage der Stadt München [2] scheint die Migration zu Linux eine Erfolgsgeschichte zu sein. Man darf sich derzeit fragen, warum überhaupt diese Diskussionen vom neuen Bürgermeister losgetreten wurden. Ein Schelm, wer hier an die Lobbyisten von Microsoft denkt.

Ehrlich gesagt fände ich es ausgesprochen positiv, wenn München bei Open-Source bliebe. In anderen Ländern innerhalb Europas scheint man ja auch den Weg gehen zu wollen. In Turin [3] steigt man von Windows auf Linux und die französische Gendarmerie [4] (es wird mit 40% Einsparung gerechnet) stellt gleichfalls auf Linux um. Womöglich handelt es sich hier also eher um einen Trend und aus diesem Grund sollte München eventuell froh sein, bereits so weit zu sein.

Open-Source ist ein bedeutsamer Gegenpol und die letzte Chance für die europäische Softwareindustrie - im Vergleich zur amerikanischen. Auch bei anderen Themen, wie Industrie 4.0 wird ebendies ein relevanter Punkt sein. Nur mit Open-Source ist es bei diesem Thema bspw. dem deutschen Maschinenbau möglich, von namhaften Softwareanbietern unabhängig zu bleiben. Dies ist um so essenzieller, da zukünftig davon auszugehen ist, dass der Maschinenbau mit digitalen Services (bspw. Predictive Analytics) rund um seine Maschinen ein überaus lukratives Zusatzgeschäft aufbauen kann.

[1] http://www.sueddeutsche.de/muenchen/software-streit-im-muenchner-rathaus-eine-frage-der-einstellung-1.2093653
[2] http://www.muenchen.de/rathaus/Stadtverwaltung/Direktorium/LiMux.html
[3] http://www.heise.de/newsticker/meldung/Turin-steigt-von-Windows-XP-auf-Linux-um-2391613.html
[4] http://www.wired.com/2013/09/gendarmerie_linux/

Montag, 1. September 2014

Automatische Tests des User Interface einer Web Applikation: Tipps und Tricks mit Se Builder (Teil 4)

Diese 6-teilige Blogpost-Reihe befasst sich mit dem Thema Test-Automatisierung für grafische Benutzeroberflächen von Webanwendungen. Im Wesentlichen kommt die Selenium Test Tool Suite zum Einsatz. Im Speziellen wird der Umgang mit dem Tool Se Builder erklärt, mit dem Tests aufgenommen und abgespielt werden können.

Der erste Teil [1] gab eine kurze Einführung zum Tool Selenium Builder, die Installation sowie einen Vergleich des Tools mit der Selenium IDE. Im zweiten Teil [2] haben wir das Selenium JSON-Format untersucht und eine Test-Suite erstellt. In diesem Post erfolgt die Fortsetzung von Tipps und Tricks aus Teil 3 [3] der Serie.

In den weiteren Teilen der Blogpost Reihe geht es dann um das Client-Server-Szenario mit Selenium Grid und um automatisierte Tests zusammen mit dem CI Server Jenkins.

Dialog-Box Beispiele

Das Thema "Dialog-Boxen" wird in der Selenium Community immer wieder diskutiert. Hauptsächlich weil die diversen Anwender von unterschiedlichen Elementen sprechen. Im Wesentlichen gibt es zwei Arten von Dialog-Boxen:
  1. Der Dialog wird vom Betriebssystem geworfen
    Eine Dialog-Box des Betriebssystems ist außerhalb des Anwendungsbereichs von Selenium. Damit kann diese mit Selenium nicht abgefangen werden. Davon betroffen ist insbesondere der „Datei öffnen“-Dialog beim Upload einer Datei auf einen Webserver.
  2. Der Dialog wird von JavaScript geworfen
    Die Dialog-Box von JavaScript befindet sich im Kontext des Browsers und kann daher von Selenium abgefangen werden. Dies gilt insbesondere für Bestätigungen und Warnungen.
Im Se Builder gibt es folgende Methoden, um einen Alert abzufangen:
  • answerAlert
  • acceptAlert
  • dismissAlert
Außerdem gibt es folgende Methoden, um einen Alert zu behandeln:
  • AlertText
  • AlertPresent
Diese sind jeweils für die Methoden-Gruppen Assertion, Verify, Wait und Store verfügbar. Das aufgeführte Beispiel zeigt ein Testskript mit einem Confirm-Dialog als Demo [4]:
{
  "type": "script",
  "seleniumVersion": "2",
  "formatVersion": 2,
  "steps": [
    {
      "type": "get",
      "url": "http://sislands.com/coin70/week1/dialogbox.htm#confirm"
    },
    {
      "type": "clickElement",
      "locator": {
        "type": "xpath",
        "value": "//div/center/table/tbody/tr/td/form[3]/p/input"
      }
    },
    {
      "type": "dismissAlert"
    },
    {
      "type": "acceptAlert"
    },
    {
      "type": "get",
      "url": "http://blog.exensio.de/"
    }
  ],
  "data": {
    "configs": {},
    "source": "none"
  },
  "inputs": [],
  "timeoutSeconds": 60
}
Im Beispiel wird der Dialog abgelehnt. Die Aktion wird mit einem zweiten Dialog quittiert.
Das Abfangen eines Alerts mit Selenium (z. B. mit "acceptAlert") funktioniert zuverlässig. Allerdings verliert Se Builder nach dem Bestätigen des Alerts teilweise den Fokus zum aktiven Browser-Fenster. Mein Test-Skript kann nach einem solchen Aufruf nicht mehr zum aktiven Browser-Fenster zurückkehren und bricht typischerweise ab.
Als Lösung teile ich meine Testfälle, die eine solche "Alert"-Methode enthalten, immer in mehrere kleine Testfälle auf, die ich dann als Test-Suite speichere. Dieses Vorgehen hat bisher zuverlässig funktioniert, denn beim Wechsel zu einem neuen Testfall innerhalb einer Suite, erhält der Browser den Fokus erneut.

ComboBox Beispiele

In Claretportal wird das Element ComboBox u. a. für die Auswahl der maximal angezeigten Datensätze bei der Paginierung verwendet. Möchte ein Anwender diese maximale Anzahl ändern, klickt man zunächst die ComboBox an, woraufhin sie sich öffnet. Danach klickt man den gewünschten Wert an, woraufhin sich die ComboBox wieder schließt. Aus Sicht von Selenium gibt es für die ComboBox zwar einen Locator bzw. einen XPath. Aber die einzelnen Werte der ComboBox besitzen keinen XPath. Um sie dennoch mit Selenium auswählen zu kennen, verwendet man einen passenden CSS-Selector:
option[value='<Selektierter Wert der Combobox>']
Um die maximale Anzahl angezeigter Datensätze des Objekts "News" bei der Paginierung auf "10" zu setzen, sind also folgende beiden Schritte im Testskript erforderlich:
{
  "type": "clickElement",
  "locator": {
    "type": "id",
    "value": "news_max_selection"
  }
},
{
  "type": "clickElement",
  "locator": {
    "type": "css selector",
    "value": "option[value='10']"
  }
}
Dabei hat das Element ComboBox im Beispiel die ID "news_max_selection". Um den Testfall generisch zu gestalten, kann man zuvor natürlich über die Methode storeEval und die JavaScript -Funktionen .length sowie .value die Werte der ComboBox abfragen und abspeichern.

Internet Explorer 8

Eine nach wie vor gängige Anforderung in Unternehmen ist die Unterstützung der Version 8 des Internet Explorers von Microsoft. Wir verwenden dazu in unserer Testumgebung Virtuelle Maschinen (VM) von modern.IE [5] (dazu mehr im letzten Teil dieser Blogpost Reihe). In der Hauptsache traten die folgenden beiden Schwierigkeiten auf:
  • JavaScript-Funktion .getElementsByClassName
  • Unterschiede bei RGB Angaben
Im Claretportal erhält nicht jedes Element eine eigene ID. So können manche Elemente nicht über die ID angesprochen werden, sondern z. B. über Ihre Position im Document Object Model (DOM). Dafür hatte ich anfangs die .getElementsByClassName Funktion verwendet. Die Testfälle konnten im Firefox fehlerfrei abgespielt werden. Unter Internet Explorer 8 (IE8) kam es allerdings zu JavaScript Fehlern. Wie sich zeigte ist diese Methode im IE8 nicht implementiert [6]. Als Abhilfe bietet sich die Verwendung der Methode .querySelectorAll an [7]. Der Aufruf im Skript sieht dann wie folgt aus:
{
  "type": "storeEval",
  "script": "var index = document.querySelectorAll('.pagination')[0].getElements \
       ByTagName(\"a\").length; var link = \
       document.querySelectorAll('.pagination')[0].getElements \
       ByTagName(\"a\")[index-2].innerHTML;return link;",
  "variable": "link"
}
Eine weitere Stolperfalle waren Farbangaben im RGB-Format. Im Claretportal gibt es die Möglichkeit, eigene Favoriten zu setzen. Ein neu angelegter Favorit, also ein Link, wird in schwarzer Farbe dargestellt. Sobald man den Favoriten das erste Mal geöffnet hat, wird der Link in grauer Farbe dargestellt. Ich habe mich für die Verwendung der Methode storeElementStyle entschieden. Per CSS Selector identifiziere ich den Favoriten und als propertyName gebe ich "color" an. Danach verwende ich die Methode assertEval, um den gespeicherten Rückgabewert mit meiner Vorgabe "rgba(8,117,198,1)" zu vergleichen. Als Stolperfalle erwies sich das unterschiedliche Rückgabeformat des RGB-Werts. Firefox (und andere Browser) geben den Wert "rgba( 8, 117, 198, 1)" zurück. Vom Internet Explorer 8 erhält man dagegen den Wert "rgba(8,117,198,1)". Als Abhilfe rufe ich in der Methode assertEval ein kleines Skript auf, welches bei meinem Rückgabewert zuerst alle Leerzeichen entfernt und danach die beiden Werte vergleicht. Die beiden Schritte des Skripts sehen dann wie folgt aus:
{
  "type": "storeElementStyle",
  "locator": {
  "type": "css selector",
  "value": "b"
  },
  "propertyName": "color",
  "value": "retval"
},
{
  "type": "assertEval",
  "script": "retval = '${retval}'; retval = retval.replace(/\\s/g, \"\"); \
if ( retval == 'rgba(8,117,198,1)' ) { return 1; };",
  "value": "1"
}
Damit lassen sich Farbangaben, auch bei unterschiedlichen Browsern, zuverlässig miteinander vergleichen.

Pausen einplanen

Beim Aufzeichnen von Testfällen registriert Se Builder keine Pausen. Ein nachträglich manuelles Setzen ist notwendig. Bei meiner Arbeit hatte ich immer wieder das Phänomen, dass ich aufgezeichnete Testfälle zwar erfolgreich lokal abspielen konnte. Sobald ich aber mehrere einzelne Testfälle gemeinsam als Test-Suite abgespielt habe, gerieten einzelne Testfälle ins Stocken. Erschwerend kam dazu, dass die einzelnen Testfälle teilweise aufeinander aufbauen. Se Builder stoppt nur in manchen Fällen die ganze Suite, wenn es bei einzelnen Testfällen zu einem Fehler kommt. Andernfalls wird im Fehlerfall der aktuelle Testfall abgebrochen und der nächste Testfall der Suite begonnen. Noch gravierender war das Verhalten im Grid, also bei Ausführung auf einem entfernten PC. Zuerst habe ich versucht, diejenigen Stellen in meiner Test-Suite ausfindig zu machen, die für die Unterbrechung verantwortlich sind. Allerdings waren die Stellen nicht bei jedem Durchlauf identisch, sondern scheinbar zufällig verteilt. Dies führte mich dazu, dass ich begonnen habe, jeden einzelnen Schritt jedes Testlaufs mit einer Pause zu verzögern. In Selenium IDE gabe es dafür den „Geschwindigkeitsregler“, auf den aber in Se Builder verzichtet wurde. Mit diesem Regler konnte man die Abspielgeschwindigkeit von Selenium IDE beeinflussen. Bei häufigen Abbrüchen der Testläufe war es ratsam, die Geschwindigkeit zu verringern. Da dies bei Se Builder nicht mehr möglich ist, kann man entsprechend viele Pausen setzen um so die Geschwindigkeit zu verringern. Mein Skript sieht dann beispielsweise so aus:
{
   "type": "pause",
   "waitTime": "500"
},
{
  "type": "waitForTextPresent",
  "text": "Main > Info Center"
},
{
  "type": "pause",
  "waitTime": "500"
},
{
  "type": "assertTextPresent",
  "text": "Main > Info Center"
},
{
  "type": "pause",
  "waitTime": "500"
},
{
  "type": "setElementText",
  "locator": {
"type": "id",
"value": "q"
  },
  "text": "Suchstring"
},
{
  "type": "pause",
  "waitTime": "500"
},
{
  "type": "clickElement",
  "locator": {
"type": "css selector",
"value": "#searchForm > input.search-submit"
  }
}
Auf diese Weise laufen die Skripte recht zuverlässig und vollständig durch. Im Allgemeinen verwende ich einen Pausenwert von 500 Millisekunden. An der ein oder anderen Stelle kann es erforderlich sein, den Wert auf 1.500 zu erhöhen.

Screenshots

Zur Kontrolle des Inhalts des Webbrowsers beim Durchlaufen eines Testfalls und insbesondere auch zur Fehlersuche, kann es hilfreich sein, Screenshots zu untersuchen. Mit der Methode saveScreenshot kann man Se Builder anweisen, einen Screenshot der aktuellen Webseite zu erstellen. Als Argument erwartet die Methode den Pfad zur Bilddatei. Bei dem Pfad kann es sich um einen UNC-Pfad handeln, so dass man z. B. ein zentrales Netzlaufwerk im Firmennetzwerk angeben kann. Bei der Benennung der Dateinamen habe ich mich für eine Kombination aus Datum und Name des Testlaufs entschieden. Auf diese Weise kann ich meine Screenshots gut sortieren und trotzdem einfach feststellen, wann und wo das Bildschirmfoto aufgenommen wurde. Mein Funktionsaufruf zur Erstellung von Screenshots sieht wie folgt aus:
{
  "type": "storeEval",
  "script": "var mm = new Array('01', '02', '03', '04', '05', '06','07', '08', '09', \
      '10', '11','12');var now = new Date();var yy = now.getFullYear();var m = now. \     
       getMonth();var dd = now.getDate();var HH = now.getHours();var MM = now.get \  
       Minutes();var SS = now.getSeconds();var MS = now.getMilliseconds();var  date= \ 
       (yy+mm[m]+dd+HH+MM+SS);return date;",
  "variable": "retval"
},
{
  "type": "saveScreenshot",
  "file": "\\\\share\\Public\\\\Screenshots\\${retval}_Testfall1.png"
}
Im nächsten Teil dieser Reihe werden wir einen Blick auf Selenium Grid werfen. Damit lassen sich Tests in einem Grid auf unterschiedlichen Knoten mit unterschiedlichen Browsern abspielen.

Links

[1] http://blog.exensio.de/2014/07/automatische-browsertests-mit-selenium.html
[2] http://blog.exensio.de/2014/08/automatische-browsertests-mit-selenium.html
[3] http://blog.exensio.de/2014/08/automatische-browsertests-mit-selenium_21.html
[4] http://sislands.com/coin70/week1/dialogbox.htm
[5] http://loc.modern.ie
[6] http://msdn.microsoft.com/en-us/library/ie/ff975198%28v=vs.85%29.aspx
[7] http://msdn.microsoft.com/en-us/library/ie/cc304115%28v=vs.85%29.aspx


Donnerstag, 21. August 2014

Automatische Browsertests mit Selenium: Tipps und Tricks mit Se Builder (Teil 3)

Diese 6-teilige Blogpost-Reihe befasst sich mit dem Thema Test-Automatisierung für grafische Benutzeroberflächen von Webanwendungen. Im Wesentlichen kommt die Selenium Test Tool Suite zum Einsatz. Im Speziellen wird der Umgang mit dem Tool Se Builder erklärt, mit dem Tests aufgenommen und abgespielt werden können.

Der erste Teil [1] gab eine kurze Einführung zum Tool Selenium Builder, die Installation sowie einen Vergleich des Tools mit der Selenium IDE. Im zweiten Teil [2] haben wir das Selenium JSON-Format untersucht und eine Test-Suite erstellt.

In diesem Teil gebe ich einige nützliche Tipps zum Erstellen von umfangreichen Test-Suiten. Es werden auch Tricks zum Umgang mit typischen Elementen eines Intranetportals verraten.
In den weiteren Teilen der Blogpost-Reihe geht es schließlich um das Client-Server-Szenario mit Selenium Grid sowie automatisierte Tests im Zusammenspiel mit dem CI Server Jenkins.

Herausforderungen mit Selenium Builder

Zum Kernprodukt von exensio gehört das Intranetportal Claretportal. Der große Funktionsumfang muss ausgiebig und zuverlässig getestet werden, um eine entsprechend hohe Softwarequalität des Produkts zu gewährleisten. Se Builder ist bei funktionalen Tests eine gute Unterstützung. Jedoch kann man bei der Erstellung von Testfällen einigen Tücken begegnen. Im vorliegenden Fall sind dies z.B.:
  • Die Verwendung des WYSIWYG Editor CKEditor, der in einem Popup geladen wird
  • Tooltips der Klasse jQuery Tooltipsy
  • JavaScript Menüs, die beim Überfahren mit dem Mauszeiger angezeigt werden
  • Testfallübergreifende Zustände
  • Dialog-Box Beispiele
  • ComboBox Beispiele
  • Sicherstellen der Verwendbarkeit bestimmter Browser und bestimmter Versionen, hier Internet Explorer 8
  • Einplanung von Wartezeiten

Der WYSIWYG Editor CKEditor

Der Editor wird im Claretportal üblicherweise per JavaScript in die Webseite eingebunden und steht als Objekt CKEDITOR zur Verfügung.
Probleme gibt es sowohl beim Test von Texteingaben, als auch beim Auslesen von Textinhalten des Editors. Bei Se Builder und WebDriver Tests kann Text nicht direkt in das Textfeld des Editors eingegeben werden. Beim Anlegen eines Textes ist das Textfeld hinter einem iFrame versteckt, so dass man nicht direkt darauf zugreifen kann. Beim Editieren eines bestehenden Textes ist das Textfeld zwar sichtbar und man kann direkt darauf zugreifen, allerdings dient dieser Text nur zur Anzeige. Änderungen an diesem Textfeld gehen beim Speichern verloren. Die Lösung besteht darin, per JavaScript auf die Instanz des Editors zuzugreifen. Im Selenium Builder geht das mit der Methode storeEval. Im Folgenden wird im 'Step 1' des JSON Skriptes von Selenium Builder die Instanz 'text' des CKEditors angelegt und der Text 'Automatisch eingefügter Text.' eingefügt.
{
  "type": "storeEval",
  "script": "CKEDITOR.instances['text'].setData( '<p>Mein Text.</p>');",
  "variable": ""
}
Die Methode "storeEval" akzeptiert als Argument "script" einen JavaScript-Ausdruck. Mit dem Aufruf "return" kann bei Bedarf ein Rückgabewert des JavaScript Programms zwischengespeichert werden. Im Argument "variable" würde dann der Name der Variablen für die spätere Verwendung festgelegt. Da wir keine Variable speichern wollen, können wir den Wert des Arguments leer lassen.
Beim Auslesen des Textinhalts des Editors hat Selenium Builder scheinbar ein Problem mit Zeilenumbrüchen ('\n' und '<p></p>') in Strings. Mittels Attribut "value" bzw. "GetData()" bekommt man den Wert des CKEditors zurück. Dieser Wert enthält allerdings Zeilenumbrüche. Will man den Wert direkt in Selenium Builder weiterverwenden, führt dies zu einem Fehler beim Ausführen des Skripts. Eine Lösung besteht im Ersetzen der Zeilenumbrüche vor dem Speichern der Variablen.
{
  "type": "storeEval",
  "script": "var retval = document.getElementById('text').value;retval = retval.replace(\"\",\"\");retval = retval.replace(\"\",\"\");retval = retval.trim();return retval;",
  "variable": "retval"
},
{
  "type": "storeEval",
  "script": "return CKEDITOR.instances['text'].setData('<p>' + String('${retval}' + '. Dieser Satz wurde nachträglich hinzugefügt.</p>') );",
  "variable": ""
}
Ich verwende in diesem Beispiel wieder die Methode"storeEval". Dieses Mal jedoch mit dem Aufruf "return" um den Rückgabewert JavaScript-Codes zwischenzuspeichern. Mit dem Wert "retval" des Arguments "variable" wird der Variablennamen für die spätere Verwendung des Rückgabewerts bezeichnet. Ich rufe die Methode "storeEval" zwei Mal nacheinander auf. Im ersten Schritt lese ich den Wert des Textinhalts des Editors aus und ersetze bestimmte Zeichen. Im zweiten Schritt erfolgt das Schreibe des Rückgabewertes aus Schritt 1 wieder zum Editor sowie das Einfügegen des eigenen Textes. Hier sieht man, wie im Argument "script" der Methode "storeEval" auf eine Variable des Se Builders zugegriffen wird. Im Beispiel verwende ich die Variable "retval" durch den Aufruf "${retval}".

jQuery Tooltipsy

Das jQuery Tooltip Plugin tooltipsy ist bei Webentwicklern beliebt, weil es eine schnelle und einfache Möglichkeit zur Darstellung von Tooltips bietet. Bei Se Builder gibt es hierfür die Methode "mouseOverElement" im Abschnitt "Input", die als Argument eine ID benötigt. Im Fall von Claretportal ist das jQuery plugin tooltipsy wie folgt eingebunden:

Bei der Aufnahme des Test-Skripts mit Se Builder wird deshalb ein CSS Selector verwendet, der u.a. auch die zufällige ID enthält. Im weiteren Verlauf der Aufnahme (mit der selben Session-ID, also ohne den Browser zu beenden) scheint vorerst alles zu funktionieren. Der verwendete CSS Selector ist allerdings nicht Session-übergreifend valide, so dass die Tests beim nächsten Öffnen des Browsers nicht mehr funktionieren. Die genauere Untersuchung des Elements, welches den Tooltip enthält, z.B. mit der Firefox Erweiterung FirePath, führt zu einem besser geeigneteren XPath. Der Test sieht dann wie folgt aus:
{
  "type": "mouseOverElement",
  "locator": 
    {
    "type": "xpath",
    "value": "//div[1]/div[3]/div/div/div/table/tbody/tr[2]/td[1]/div[1]/div/table/tbody/tr[1]/td[2]/span/img"
    }
},
{
  "type": "waitForTextPresent",
  "text": "Achtung"
}
Dabei ist es unbedingt empfehlenswert, den zu kontrollierenden Text des Tooltips mit der Methode "waitForTextPresent" zu prüfen. Denn die Methode "mouseOverElement" bewirkt nicht immer sofort das Erscheinen des Tooltips. Mitunter vergehen einige Sekunden, bis der Tooltip in der Art angezeigt wird, dass die Prüfung per "verifyTextPresent" funktioniert.

JavaScript Menüs

In Claretportal gibt es eine Hauptnavigation mit Menüeinträgen, die bei MouseOver Ereignissen angezeigt werden. Zunächst wurde versucht, mittels der Methode "mouseOverElement" ein Menü anzuzeigen. Dieser Versuch war allerdings nicht von Erfolg gekrönt. Ich habe unterschiedliche Lokatoren verwendet (CSS Selector, 2 x XPath, link text). Das Skript wurde immer erfolgreich ausgeführt. Allerdings wurde zu keinem Zeitpunkt das Untermenü der Hauptnavigation angezeigt. Danach habe ich versucht, den entsprechenden Menü-Eintrag des Untermenüs direkt mit der methode "clickElement" und dem bekannten CSS-Selector bzw. dem XPath anzusprechen. Allerdings verhielt sich WebDriver hier völlig richtig und quittierte diese Versuche mit der Meldung "Unable to locate element". Das Problem waren die nicht sichtbaren Menüs. Der zugehörige html Style ist "style.display = none". Als Lösung habe ich also eine "storeEval" Methode aufgerufen und darin folgenden JavaScript Code ausgeführt:
var drops = document.getElementsByClassName('drop').length;
In einer zweiten “storeEval” Methode habe ich dann diesen Code ausgeführt und damit das entsprechende Menü sichtbar geschaltet:
document.getElementsByClassName('drop')[${index}-1].style.display = 'block';
Die beiden Schritte sehen im JSON-Skript wie folgt aus:
{
  "type": "storeEval",
  "script": "return document.querySelectorAll('.drop').length;",
  "variable": "drops"
},
{
  "type": "storeEval",
  "script": "document.querySelectorAll('.drop')[${drops}-1].style.display = 'block';",
  "variable": ""
}
Anstatt "-1" statisch in den Quellcode des Test-Skripts zu schreiben, könnte dort auch eine Variable wie z. B. "${index}" verwendet werden. Dadurch wäre der Testfall flexibler.

Testfallübergreifende Zustände

Im zweiten Teil der Blogpost Reihe haben wir gesehen, dass es ratsam sein kann, einen umfangreichen Testfall in kleinere Testfälle aufzugliedern und diese in einer Test-Suite zu gruppieren. Im Folgenden verwende ich die einfache Test-Suite:
{
  "type": "suite",
  "seleniumVersion": "2",
  "formatVersion": 1,
  "scripts": [
    {
      "where": "local",
      "path": "test_blogpost_teil_3_schritt1.json"
    },
    {
      "where": "local",
      "path": "test_blogpost_teil_3_schritt2.json"
    }
  ],
  "shareState": true
}
Die Suite startet den Testfall "test_blogpost_teil_3_schritt1.json":
{
  "type": "script",
  "seleniumVersion": "2",
  "formatVersion": 2,
  "steps": [
    {
      "type": "get",
      "url": "http://blog.exensio.de/"
    },
    {
      "type": "storeText",
      "locator": {
        "type": "css selector",
        "value": "h2.title"
      },
      "variable": "teaser"
    }
  ],
  "data": {
    "configs": {
      "none": {}
    },
    "source": "none"
  },
  "inputs": [],
  "timeoutSeconds": 60
}
Außerdem wird der Testfall "test_blogpost_teil_3_schritt2.json" gestartet:
{
  "type": "script",
  "seleniumVersion": "2",
  "formatVersion": 2,
  "steps": [
    {
      "type": "setElementText",
      "locator": {
        "type": "name",
        "value": "search"
      },
      "text": "${teaser}"
    }
  ],
  "data": {
    "configs": {
      "none": {},
      "manual": {
        "retval": "Das ist ein Test"
      }
    },
    "source": "manual"
  },
  "inputs": [],
  "timeoutSeconds": 60
}
Mit den Standard-Einstellungen von Se Builder kann die Suite so nicht erfolgreich abgespielt werden. Beim Abspielen des zweiten Testfalls erhält man die Fehlermeldung "Variable not set: teaser". Das mag im ersten Augenblick verwundern, weil die Variable "teaser" im ersten Testfall explizit gesetzt wurde. Das Problem liegt an den Standard-Einstellungen von Se Builder, die beim Abspielen einer Suite die Zustände der einzelnen Testfälle streng voneinander unterschiedet. Das bedeutet, dass Variablen, Sessions und Cookies immer nur im aktuellen Testfall verwendet werden können. In manchen Szenarien mag diese Einstellung richtig sein. In unserem aktuellen Fall wollen wir den Zustand eines Testfalls auf einen anderen übertragen. Dazu setzt man im Se Builder Run Menü die Option "Share state across suite". Danach kann die Suite vollständig und erfolgreich abgespielt werden.

Der nächste Teil [3] dieser Reihe ist eine Fortsetzung der Tipps und Tricks. Darin werde ich den Umgang mit einer AlertBox beleuchten, eine Auswahl in einer ComboBox treffen und viele Pausen setzen. Es werden zwei Beispiele für die Gestaltung von Testfälle mit dem Internet Explorer 8 gezeigt.

Links

[1] Automatische Browsertests mit Selenium: Zielvorstellung und Einführung Selenium Builder (Teil 1)
[2] Automatische Browsertests mit Selenium: JSON Dateiformat und Test-Suite (Teil 2)
[3] Automatische Tests des User Interface einer Web Applikation: Tipps und Tricks mit Se Builder (Teil 4)

Montag, 18. August 2014

Szenarien für den Einsatz von IoT und BigData: Visualsierung von großen Datenmengen (Teil 6)

Im Teil 5 dieser Blog-Post Serie wurde auf die Visualisierung von Sensordaten mit Hilfe der JavaScript-Bibliothek D3.js eingegangen. In diesem Teil möchte ich nun auf den Umgang mit großen Datenmengen am Beispiel der erfassten Wetterdaten eingehen.


Als Ziel sollen alle bisher aufgezeichneten Messdaten in einem Liniendiagramm dargestellt werden. Das Diagramm soll auch bei großen Datenmengen vergleichbar rasch verfügbar sein. Zum Erstellen der Grafiken kommt wiederum die auf D3.js basierende Bibliothek Rickshaw zum Einsatz. Als Beispiel eines solchen Diagramms werden die bisher aufgezeichneten Messpunkte der Außentemperatur dargestellt.

Mit der folgenden Query ist eine Ermittlung und Aufbereitung aller Messpunkte der Außentemperatur für die Grafik möglich. Es wird hierzu wie im letzten Teil dieser Serie schon gezeigt, die "Date Histogramm" Aggregation verwendet.

 client.search({
        index: 'sensorraspberry',
        size: 0,
        body: {
            // Begin query.
             "query":{
                 "bool" : {
                     "must" : [
                         {
                             "term": {
                                 "type": "outdoor"
                             }
                         },
                         {
                             "range" : {
                                 "created" : {
                                     "gte": "now-1y"
                                 }
                             }
                         }
                     ]
                 }
              },
              "aggs" : {
                  "temp_over_time" : {
                      "date_histogram" : {
                          "field" : "created",
                          "interval" : "1m"
                      },
                      "aggs" : {
                        "temp" : { "avg" : { "field" : "temperature" } }
                      }          
                  }
              }
            // End query.
        }


Das Laden dieser Grafik benötigt aufgrund der Größe der Daten relativ lange. Des Weiteren sieht die Darstellung "kantig" aus, da jeder einzelne Messwert aufgeführt wird. Bei Ansichten über längere Zeiträume sind gemittelte Werte für die Visualisierung ausreichend. Hierdurch ist ein rascheres Laden der Daten und eine ansprechendere Darstellung möglich. Es werden nun über 12h-Intervalle gemittelte Werte angezeigt. Dies ermöglicht die Änderung des Intervalls für die Aggregation.

"aggs" : {
                  "temp_over_time" : {
                      "date_histogram" : {
                          "field" : "created",
                          "interval" : "12h"
                      },
                      "aggs" : {
                        "temp" : { "avg" : { "field" : "temperature" } }
                      }          
                  }
              }


Interesse besteht natürlich nicht nur an gemittelten Werten, sondern auch an den einzelnen Messwerten. Aus diesem Grund ist beim Hineinzoomen das Anzeigen von exakteren Werten notwendig. Das Hineinzoomen in die Grafik wird über die bereits durch Rickshaw mitgelieferte Scrollbar realisiert.


Der angezeigte Zeitbereich kann über Graph.window.xMin bzw. Graph.window.xMax abgefragt werden. Ist die Zeitspanne kleiner als ein definiertes Intervall, z.B. ein Monat, wird die Query angepasst. In diesem Zeitbereich kann beispielsweise über eine Stunde gemittelt werden. Die zurück gelieferten Messwerte werden in ein Datenarray hineingeschrieben und nach Zeitstempel sortiert. Danach erfolgt die Aktualisierung der Grafik mit graph.update().

Als Ergebnis erhält man folgendes Liniendiagramm. Es lassen sich beliebig viele Auflösungsstufen einbauen und somit können auch einzelne Messwerte bei detaillierten Hineinzoomen angezeigt werden.


Wird ein Ausschnitt gewählt, der wieder größer als ein Monat ist, wird das Array auf die ursprünglichen Daten zurückgesetzt. Hierdurch erreicht man, dass das Datenarray stets eine begrenzte Anzahl an Messpunkten aufweist. Ein schnelles Laden der Grafik ist damit möglich.

Durch das schnelle Laden können Dashboards gebaut werden, die mehrere Grafiken zur Langzeitanalyse enthalten und trotzdem rasch aufgebaut werden.



Fazit

Für die Visualisierung großer Datenmenge ist es sinnvoll Werte in definierten Zeitintervallen zu mitteln, da andernfalls die Gefahr von langen Ladezeiten besteht. Um auch Daten im Detail betrachten zu können, sind Zoomfunktionen mit dynamischer Nachlade-Funktionen empfehlenswert. Exakte Messwerte können hierdurch ohne lange Ladezeiten dargestellt werden.

Donnerstag, 14. August 2014

Szenarien für den Einsatz von IoT und BigData: Visualisierung von Sensordaten mit D3.js (Teil 5)

Nachdem in den vorgehenden Teilen dieser Blog-Post Serie die Sensordaten erfasst und abgelegt wurden, möchte ich in diesem Blog-Post auf die Visualisierung der abgelegten Daten eingehen. Die Daten werden mit Hilfe der Aggregationen in Elasticsearch aufbereitet und anschließend über JavaScript entsprechend visualisiert.


Für die Visualisierung kommt die JavaScript Bibliothek D3.js zum Einsatz. Dabei handelt es sich um eine freie Javascript Bibliothek, die von Mike Bostock speziell zur Manipulation von datenbasierten Dokumenten entwickelt wurde. Des Weiteren wird die auf D3.js aufbauende Bibliothek Rickshaw verwendet, die ein flotteres Erstellen von Graphen ermöglicht.
Mit Hilfe der Visualisierung wird ein Dashboard erstellt, in dem Wetterdaten, beispielsweise die Temperatur oder Luftfeuchtigkeit der jeweiligen Sensoren, grafisch aufbereitet werden. Hierbei soll sowohl eine momentane Anzeige als auch eine Realtimegrafik die Daten anschaulich repräsentieren.
Als Beispiel für ein solches Dashboard wird hier die Visualisierung der Indoor Sensordaten gezeigt.


Für die Momentenanzeige werden Gauges, also Messgeräte verwendet. Diese zeigen den aktuellen Wert an und werden automatisch aktualisiert, wenn ein neuer Messwert eines Sensors vorhanden ist.
Die Realtimegrafik stellt die Messwerte der letzen 24 Stunden dar. Sobald ein aktuellerer Messwert verfügbar ist, wird dieser ebenfalls hinzugefügt. Die Realtime Grafik ist wie ein Sliding Window zu verstehen. Es zeigt stets nur die letzen 24h an. Daten, die älter als 24h sind, werden aus der Grafik entfernt, rutschen also aus dem Fenster heraus. Des Weiteren ist es möglich über eine Scrollbar unterhalb der Grafik in beliebige Bereiche hineinzuzoomen. Diese Funktion wird bereits durch Rickshaw selbst geliefert und benötigt keinen weiteren Programmieraufwand.

Queries

Zunächst müssen die erforderlichen Daten durch eine Query über Elasticsearch abgerufen werden. Um den prinzipiellen Aufbau einer solchen Query zu erläutern wird das Beispiel eines Gauge verwendet.
        
var client = getESClient();

 setInterval(function () {
  client.search({
   index: 'sensorraspberry',
   body: { 
     "query": {
    "bool": {
      "must": [
     {
       "term": {
      "type": "indoor"
       }
     },
     {
       "range": {
      "created": {
        "gte": "now-1d"
      }
       }
     }
      ]
    }
     },
     "aggs": {
    "devices": {
      "terms": {
     "field": "device",
     "order": {
       "_term": "asc"
     }
      },
      "aggs": {
     "time": {
       "terms": {
      "field": "created",
      "order": {
        "_term": "desc"
      },
      "size": 1
       },
       "aggs": {
      "temp": {
        "avg": {
       "field": "temperature"
        }
      }
       }
     }
      }
    }
     },
     "size": 0
   }
   }


Zunächst wird der Index festgelegt, aus dem die Daten abgefragt werden. Wird kein Index festgelegt, so wird über alle Indizes die Suche gestartet. Die Suche kann über mehrere Suchkriterien eingeschränkt werden. In diesem Beispiel müssen die Suchergebnisse vom Typ „indoor“ sein. Außerdem sollen nur Daten des letzten Tages gefunden werden. Folglich werden alle notwendigen Daten zur späteren Visualisierung gefunden.
Aufgrund der Anzahl der Messergebnisse ist es sinnvoll die Daten zu gruppieren. Hierzu bietet Elasticsearch die Aggregationen an. Diese ermöglichen es Suchergebnisse direkt in verschiedene „Töpfe“ (buckets) einzusortieren. Eine Aggregation kann beliebig verschachtelt sein und über mehrere Optionen frei konfiguriert werden. In diesem Beispiel wird für jeden Sensor (device) ein eigener Topf erstellt. In diesen Töpfen werden die Daten nach dem Erstellungsdatum sortiert und nur der neuste Wert angezeigt. Die maximale Anzahl der zurückzuliefernden Werte lassen sich über „size“ festlegen.

Erstellen des Gauge

Das Gauge für die Temperatur kann über JavaScript erstellt werden. Verschiedene Optionen, wie beispielsweise die Größe oder auch Farbbereiche können festgelegt werden.

 google.load('visualization', '1', {packages: ['gauge']});
    google.setOnLoadCallback(function () {
        var data1 = google.visualization.arrayToDataTable([
            ['Label', 'Value'],
            [String.fromCharCode(186) + 'C', 0]
        ]);
 
        // Create and draw the visualization.
        var options1 = {
            width: 200,
            max: 50,
            redFrom: 40,
            redTo: 50,
            yellowFrom: 30,
            yellowTo: 40,
            greenFrom: 20,
            greenTo: 30,
            minorTicks: 10,
            majorTicks: ['0', '10', '20', '30', '40', '50']
        };
        var gaugechart1 = new google.visualization.Gauge(document.getElementById('visual_temp_server'));
        gaugechart1.draw(data1, options1);

Dies reicht aus um einen Gauge zu erstellen. Das Ergebnis ist im folgenden Bild zu sehen.


Query für Realtimegrafiken

Um einen Verlauf von Messpunkten zu visualisieren benötigt man die Daten nach Datum bzw. Uhrzeit sortiert. Für diesen Anwendungsfall bietet Elasticsearch eine spezielle Aggregation, die sogenannte „Date Histogram Aggregation“. Sie unterteilt die Suchergebnisse in frei wählbare Zeitabschnitte (Intervalle).

"aggs": {
 "devices": {
   "terms": {
  "field": "device",
  "order": {
    "_term": "asc"
  }
   },
   "aggs": {
  "time": {
    "terms": {
   "field": "created",
   "order": {
     "_term": "desc"
   },
   "size": 1
    },
    "aggs": {
   "temp": {
     "avg": {
    "field": "temperature"
     }
   }
    }
  }
   }
 }
},
 
In unserem Beispiel wird von den Sensoren alle 2 Sekunden ein Wert geliefert. Dies führt zu einer großen Anzahl an Daten. Für die Visualisierung genügt es gemittelte Werte, z.B. über Minuten, anzuzeigen. Die Query des Gauge muss nur leicht verändert werden, um die notwendigen Daten zu erhalten. Diese werden  anschließend in einem Array ablegt.

Erstellen der Grafik mit Messpunkteverlauf

Um die nun gewonnenen Messpunkte zu visualisieren, kommt die schon erwähnte Bibliothek Rickshaw zum Einsatz. Es muss lediglich der Graph erstellt und mit den Daten befüllt werden. Optionen welche Art von Graph bzw. auch welche Farben welchen Daten entsprechen sollen, lassen sich beliebig zuweisen.

var graph = new Rickshaw.Graph( {
              element: document.getElementById("chart_temp_indoor"),
              height: 300,
              renderer: 'line',
              stroke: true,
              preserve: true,
              series: [
              {color: 'steelblue', data: dataserver, name: 'Serverroom'},
              {color: 'green', data: dataroom1, name:'Office'},
              {color: 'red', data: dataroom3, name: 'Meeting room'},
              {color: 'brown', data: outside, name: 'Outdoor'},
              ]
              } );


Verschiedene Zusätze wie Legenden oder die Scrollbar können optional hinzugefügt werden. Das Ausführen des JavaScript führt zu folgender Darstellung.

 
Um nun zu erreichen, dass aus dem statisch aufgebauten Graphen eine Realtime Anzeige der letzen 24h wird, müssen neue Daten hinzugefügt, bzw. ältere Daten aus der Grafik entfernt werden. Hierzu wird die JavaScript Funktion setInterval() verwendet. In dieser wird in definierbaren Zeitabständen eine Query abgesetzt, die lediglich die neusten Werte der Sensoren als Ergebnis zurückbekommt. Die Query ist somit identisch mit der Gauge-Query .
Liegt nun ein neuer Messwert vor wird dieser mit der Arrayfunktion push(neuerMesswert) zu dem jeweiligen Datenarray hinzugefügt. Um den ältesten Messwert zu entfernen wird die Funktion shift() verwendet.
Sind die neuen Daten im Datenarray eingepflegt, kann die Grafik mittels graph.update() aktualisiert werden. Dies reicht bereits aus damit die Grafik Realtimefähig wird.

Fazit

Über Elasticsearch kann man einfach Queries bauen, die nach mehreren Kriterien gefiltert werden können. Die Ergebnisse der Suche lassen sich durch die Aggregation für den jeweiligen Zweck in verschiedene „Töpfe“ (buckets) gruppieren.
Durch die Verwendung der Bibliotheken D3 und Rickshaw können Daten ohne großen Programmieraufwand visualisiert werden. Da D3 auf Effizienz ausgelegt ist werden die Seiten auch mit mehreren Grafiken schnell aufgebaut.