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, 24. November 2014

Präparate Datenbank erzeugt IFA Preismeldungen

Quelle: IFA GmbH
exensio hat für einen namhaften Pharmahersteller eine Software Lösung zur Verwaltung von vertrieblichen und medizinischen Daten seiner Präparate umgesetzt. Diese Präparate-Datenbank (PIM System), die schon seit mehreren Jahren erfolgreich in Betrieb ist, wurde dieses Jahr um den IFA Preismelde- Workflow erweitert.

Die IFA (Informationsstelle für Arzneispezialitäten) erhebt und pflegt Information zu Arzneimitteln und apothekenüblichen Waren. Sie vergibt dabei die sogenannten PZN (Pharmazentralnummer), die einen Artikel eindeutig kennzeichnet. Hersteller können die Neuaufnahme sowie Änderungen der Daten an die IFA mitteilen. Hierfür existiert ein Verfahren basierend auf sogenannten EAD Dateien, die zur IFA geschickt und dort verarbeitet werden. Zurück kommt eine Bestätigung ggf. mit Korrekturen.

Preisänderungen  sind insbesondere für Generika Anbieter kritisch, da sie für den Absatz ein sehr entscheidendes Kriterium sind. Ein stabiler Workflow zur Durchführung und Übertragung der Preisänderungen ist daher erfolgskritisch.

Die Präparate-Datenbank von exensio ist nun in der Lage, basierend auf Preisanpassungen aus SAP, die durch einen SOX konformen Prozess freigegeben wurden, entsprechende IFA Meldungen automatisch zu erzeugen. Die von der IFA vorgegebenen EAD Dateien werden pro Firma des Konzerns automatisch erzeugt und rechtzeitig vor Ablauf der Einreichungsfrist an die IFA per Email verschickt.

Die IFA wiederum schickt eine Bestätigungs-Email mit Datei als Anhang zurück. Die Datei wird automatisiert in der Präparate Datenbank eingelesen und die Daten werden mit den an die IFA gesendeten Werten abgeglichen. Bei Übereinstimmung werden die geänderten Preise als von der IFA bestätigt übernommen. Bei Abweichungen werden entsprechende Warnmeldungen und Alerts erzeugt, um Folgeaktivitäten auszulösen. Alle Informationen werden an SAP zurückgespielt, um dort den fachlichen Workflow schließen zu können.

Fazit

Mit der Automatisierung gewinnt der komplette Ablauf eine hohe Sicherheit. Manuelle Schritte und damit potentielle Fehlerquellen entfallen. Die Präparate-Datenbank als Lieferant hochwertiger produktbezogener Informationen für alle Informations-Kanäle liefert damit garantiert korrekte Preisinformationen – ob in geschlossene Bereiche im Internet oder andere Intranet-Systeme.

Donnerstag, 13. November 2014

Datenanalyse mit R

Ziel dieses Blogposts ist es, an einem Beispiel zu zeigen, dass es möglich ist, aus der Programmiersprache R auf die in Elasticsearch gespeicherten Daten zuzugreifen, diese zu verarbeiten und die Ergebnisse für eine Java-Anwendung zugreifbar zu machen. Hierfür wird auf die durch Sensoren erfassten Wetterdaten zugegriffen, die bereits in diesem Blog vorgestellt.

Was ist R?

R beschreibt sich selbst als "a free software environment for statistical computing and graphics" [1]. Es bietet die Möglichkeit, Probleme aus Bereichen wie Statistik und Data Mining schnell zu lösen oder Prototypen zu bauen. R ist jedoch nicht für jedes Problem in diesen Bereichen eine gute Wahl [2]. Unsere Erfahrung mit den Wetterdaten bestätigt diese Einschätzung. Als interpretierte Sprache ist R auch nicht besonders schnell - abgesehen von den eingebauten Vektor/Matrixoperationen(hinter denen BLAS/LAPACK o.ä. steht). Um dieses Problem zu umgehen, enthalten viele R-Pakete C, C++ oder Fortran-Code, in den die rechenintensiven Operationen ausgelagert sind. Beispielsweise hat R Probleme, wenn nicht alle Daten gleichzeitig in den RAM geladen werden können.
Für die prototypische Anwendung soll der Temperaturverlauf in einem Raum für den nächsten Tag vorhergesagt werden. Es wurde hierfür der Serverraum ausgewählt.

Zugriff auf Elasticsearch

Der Zugriff auf Elasticsearch erfolgt über die REST-Schnittstelle. Um aus R HTTP-Requests senden zu können, wird das Paket RCurl verwendet. RCurl ist ein Wrapper um libcurl. Um den Response, der als JSON vorliegt, in R-Objekte zu parsen, wird das Paket RJSONIO verwendet. Das Paket jsonlite, das den selben Zweck erfüllt und besser aufgebaute R-Objekte liefert, wird nicht verwendet, da es sich als wesentlich langsamer erwiesen hat (selbst wenn man die Ausgaben von RJSONIO nachträglich umformatiert) und daher für die vorliegende Datenmenge nicht geeignet ist.
Will man R professionell an Elasticsearch anbinden, könnte es sich lohnen, aus libcurl und einem geeigneten JSON-Parser einen Client zu bauen, um das Parsen von JSON und Abbilden in R-Objekte gezielt auf Elasticsearch zu optimieren. Anhand der Abfrage weiß man im Wesentlichen, wie das zurückgegebene JSON aufgebaut ist. Da dies mit größerem Aufwand verbunden ist, wurde für die hier beschriebenen Experimente darauf verzichtet.
Nachfolgend ist die an Elasticsearch gesendete Aggregations-Query und ihre Verarbeitung dargestellt.
device <- "ex-raspberrypi-01"
type <- "indoor"
request <- paste('{
                 "query": {
                  "bool": {
                   "must": [
                    {
                     "term": { 
                      "device": "', device, '"
                     }
                    }, 
                    {"term": {"type": "', type, '"} },
                    { 
                     "range": {
                      "created" : {
                       "gte": "2014-01-01T11:00:00",
                       "lte": "2015-01-01T12:00:00"
                      }
                     }
                    },
                    { 
                     "range": {
                      "temperature": {
                       "gte": 10.0,
                       "lte": 35.0
                      }
                     }
                    }
                   ]
                  }
                 },
                 "aggs": {
                  "device_count": {
                   "terms": {
                    "field":"device"
                   },
                   "aggs": {
                    "splitminute": {
                     "date_histogram":{
                      "field":"created",
                      "interval":"hour"
                     },
                     "aggs": {
                      "avg_degrees": {
                       "avg":{
                        "field":"temperature"
                       }
                      }
                     }
                    }
                   }       
                  }
                 },
                 "from" : 0,
                 "size":1 
                }', sep='')
headers <- list('Accept' = 'application/json', 'Content-Type' = 'application/json')

h <- basicTextGatherer()

response <- curlPerform(url="http://exensiosearch02:9201/sensorraspberry/_search?pretty", postfields=request, 
                        writefunction = h$update,httpheader=headers, curl = curlhandle)
x <- h$value()

# x ist jetzt eine Zeichenkette mit der Antwort (json) - muss geparst werden.
y <- fromJSON(x)

# y besser struktieren / als data.frame zurückgeben.
data <- y$aggregations$device_count[[1]]
data <- data[[1]]$splitminute$buckets

temp <- numeric(length(data))
time <- numeric(length(data))
for(i in 1:length(data)) {
  temp[i] <- data[[i]]$avg_degrees
  time[i] <- data[[i]]$key
}
rawInput <- data.frame(created=time,  temperature=temp)
Zur Vorhersage wird ein zweischichtiges künstliches neuronales Netz verwendet. Eingaben sind die mittlere Temperatur jeder der letzten 10 Stunden und die Tageszeit/Stunde, für die vorhergesagt werden soll. Ausgeben wird die prognostizierte Temperatur für die nächste Stunde. Um beispielsweise den Temperaturverlauf über einen gesamten Tag vorzusagen, wird die Vorhersage selbst als Input verwendet, damit die Temperatur in der nachfolgenden Stunde vorhergesagt wird. Zur Erzielung besserer Ergebnisse werden die Eingabedaten geeignet skaliert.
Zum Training des neuronalen Netzes wird das Paket nnet und die Daten bis zum 1. September 2014 verwendet. Die Daten seit dem 1. September dienen zur Evaluation. Die untenstehende Abbildung zeigt die tatsächliche (rot) und die vorhergesagte Temperatur (blau) . Die Vorhersage wurde jeweils um Mitternacht auf Basis der am vergangenen Tag beobachteten Daten für den gesamten Tag erstellt. Es zeigt sich, dass die Vorhersage im letzen Drittel des Oktobers abrupt wesentlich schlechter wird. Dies liegt vermutlich an der Änderung des Standorts des Sensors im Serverraum. Vor der Umpositionierung stand der Sensor in der Nähe eines verfälschenden Abluftstrom eines Gerätes. Für diese neue Situation wurde das neuronale Netz nicht trainiert.

Ergebnisse

Es hat sich gezeigt, dass der Parameter maxit, der die Anzahl der Iterationen während des Trainings begrenzt, große Bedeutung hat: Wird maxit zu groß gewählt, neigt das Netz zu starkem overfitting, d.h. der Fehler auf den Trainingsdaten ist zwar sehr klein, aber die Vorhersagen auf dem Evaluationsdatensatz sind schlecht. Der mittlere Fehler auf den Evaluationsdaten (d.h. ab dem 1. September) beträgt 0.81 Grad Celsius.

Vorhersage aus Java abrufen

Eine Möglichkeit R von Java aus aufzurufen ist rServe. Dieses Paket ermöglicht es, R als Server zu betreiben und mittels eines Java-Clients anzusprechen. Will man R nicht als Server betreiben, sondern direkt einbinden, bietet sich alternativ RNI an. Mit rServe sieht der Java-Code wie folgt aus:
RConnection c = new RConnection();
// R-Skript einlesen und ausführen:
// - Trainingsdaten aus elasticsearch abfragen und in Variable "rawInput" speichern.
// - diverse Hilfsfunktionen zum Trainieren des neuronalen Netzes ("trainNet"), 
//    Erstellen der Prognose ("getPrognose") etc. definieren.
c.voidEval("source('.../setupPrediction.R')");
// Neuronales Netz trainieren und der Variable "knn" zuweisen.
c.voidEval("knn <- trainNet(rawInput=rawInput)");
//Das sind die mittleren Temperaturen der letzten 10 Stunden
// werden als Input für Prognose benötigt.
double[] lastTemperatures = new double[]{26.63763, 26.60047, 26.62897, 26.63891, 26.58628, 
                                         26.55079, 26.44443, 26.52929, 26.48949, 26.37};
// Werte aus Java nach R kopieren und in Variable "lastvalues" speichern.
c.assign("lastvalues", lastTemperatures);
// Temperaturprognose berechnen
// Da KNN auf skalierten Werten arbeitet, muss man Input skalieren ("scaleServerTemp") 
//  und Ergebnis zurückskalieren ("rescaleServerTemp")
REXP x = c.eval("rescaleServerTemp(getPrognose(knn, scaleServerTemp(rev(lastvalues)), 0))");
// Werte aus R wieder in Java kopieren und ausgeben.
double[] result = x.asDoubles(); 
for(int hour = 0; hour < 24; hour++) {
  System.out.println("" + hour + ":00 - " + (hour+1) + ":00 : Vorhersage: " + result[hour]);
}
Die Ausgabe sieht dann folgendermaßen aus:
0:00 - 1:00 : Vorhersage: 26.313316185020042
1:00 - 2:00 : Vorhersage: 26.275947152882623
2:00 - 3:00 : Vorhersage: 26.251989278387292
3:00 - 4:00 : Vorhersage: 26.246811159392138
4:00 - 5:00 : Vorhersage: 26.25657048628711
5:00 - 6:00 : Vorhersage: 26.279495173271123
6:00 - 7:00 : Vorhersage: 26.297577826042687
7:00 - 8:00 : Vorhersage: 26.326254511775634
8:00 - 9:00 : Vorhersage: 26.364349490351472
9:00 - 10:00 : Vorhersage: 26.39934045980503
10:00 - 11:00 : Vorhersage: 26.431707850819965
11:00 - 12:00 : Vorhersage: 26.461165964549796
12:00 - 13:00 : Vorhersage: 26.48547429218186
13:00 - 14:00 : Vorhersage: 26.503398807877936
14:00 - 15:00 : Vorhersage: 26.51429011947722
15:00 - 16:00 : Vorhersage: 26.518425475695256
16:00 - 17:00 : Vorhersage: 26.51439402371549
17:00 - 18:00 : Vorhersage: 26.501447900578995
18:00 - 19:00 : Vorhersage: 26.479813409022864
19:00 - 20:00 : Vorhersage: 26.44911167010077
20:00 - 21:00 : Vorhersage: 26.40898653790765
21:00 - 22:00 : Vorhersage: 26.35936238134047
22:00 - 23:00 : Vorhersage: 26.300194063174178
23:00 - 24:00 : Vorhersage: 26.231449480426715

Fazit

Die entwickelte Prognosefunktionalität ist sicher noch verbesserungsfähig, vor allem was die Eingaben in das neuronale Netz angeht. Es ist etwa anzunehmen, dass die Außentemperatur zumindest im Sommer einen Einfluss auf die Innentemperatur hat. Dies war jedoch nicht das Hauptziel: Es konnte gezeigt werden, dass R, Elasticsearch und Java-Anwendungen kombiniert werden können und wie man dabei vorgehen kann.

Links

[1] R-Project
[2] Joseph Adler: R In a Nutshell. Second Edition. O'Reilly. 2012

Freitag, 17. Oktober 2014

Elasticsearch und IoT

Beim Search Meetup Karlsruhe mit dem Thema "Elasticsearch Performance in a Nutshell" durfte ich neben dem Hauptvortrag von Patrick Peschlow Anwendungsszenarien zu "Elasticsearch und IoT" vorstellen.



Weitere Detail-Informationen können in der Blogpost-Reihe "Szenarien für den Einsatz von IoT und BigData" gefunden werden:

Mittwoch, 15. Oktober 2014

Big Data Expertenrunde bei Dr. Hornecker in Freiburg

Letzten Mittwoch hatte ich die Gelegenheit, bei Dr. Hornecker Software-Entwicklung und IT-Dienstleistungen über Elasticsearch im Kontext von Big Data zu referieren.

Hierbei habe ich die Vorteile von Elasticsearch und NoSQL aufgezeigt. In diesem Zusammenhang möchte ich darauf hinweisen, dass man bereits mit kleinen (strukturierten bzw. unstrukturierten) Datenmengen sinnvolle Anwendungen realisieren kann, die mit der gealterten SQL-Technologie nicht so einfach - wenn überhaupt - umsetzbar sind.



Des Weiteren habe ich folgende Einsatzszenarien für Elasticsearch skizziert:

  • NoSQL Document Store (JSON)
    • Bigdata mittels Sharding
    • Keine Transkationen im Vergleich zu MongoDB
  • Query Engine für strukturierte Daten
    • Daten werden in SQL DB gespeichert
    • Abfragen laufen über Elasticsearch
    • Vereinfachung von komplexen SQL Queries (insb. bei hierarchischen Abfragen)
  • Volltextsuche bzw. Facettierte Suche 
    • Wissensportale
    • Suche in strukturierten (Datenbanken, CRM, ERP, …) sowie unstrukturierten Daten (Content, Dokumente)
  • Geo-Suche (viel einfacher als bei Oracle DB)
    • PLZ-Umkreissuche (Filialen)
    • Flottenmanagement
    • Logistik
  • Logfile-Analyse
    • IT-Forensik
    • Sortimentslücken entdecken
  • Business Analytics (schneller und kostengünstiger vgl. mit DW)
    • Competitive Intelligence (Search Engine sehr gut geeignet, wenn Mapping über eindeutige Id wie EAN Code nicht möglich)
    • Datawarehouse (Aggregationen ersetzen Dimensionen im Star-Schema)
  • Alerts (Percolator -> indiziertes Dokument passt zu gespeicherter Query)
    • News Alerts
    • Internet Of Things (Alarm bspw. Temperatur steigt über Schwellwert)
    • Preis-Monitor
    • Online-Werbung 
Die weiteren Vorträge kamen von Dr. Michael Jachan (Brain Products) über die Grundlagen maschinellen Lernens. Und Dr. Achim Hornecker ergänzte die Runde mit dem dritten Vortrag. Er skizzierte die Entwicklung, die sich in den vergangenen Monaten ausgehend von Hadoop hin zur interaktiven Verarbeitung von großen Datenmengen bis zu Echtzeitanwendungen getan hat.

Weitere Infos auf der Homepage von Dr. Hornecker.

Dienstag, 7. Oktober 2014

Competitive Intelligence mit Elasticsearch und Kapow

In diesem Blog-Post möchte ich über ein interessantes Vorhaben - in das wir aktuell mit einer Machbarkeitsstudie involviert sind - eines bekannten deutschen Handelskonzerns berichten. In aller Kürze: Es handelt sich um die Analyse von Online-Webshops des Wettbewerbs. Ich habe - auch weil ich gerne Wein trinke :-) - für den Blog-Post als Beispiel die Konkurrenz-Beobachtung von Online-Wein-Händlern herangezogen.

Für dieses Szenario  gilt, dass die zu vergleichenden Produkte nicht über einen eindeutigen Kenner, wie bspw. den Barcode (EAN / GTIN) verglichen werden können. Dies ist der Grund, dass das Matching über eine Suchmaschine erfolgt. Das Fehlen eines unverkennbaren Identifikators wie bei Wein ist auch bei Tee oder Kaffee gegeben. Die Betrachtung von elektronischen Geräten kann hingegen fast ausschließlich mittels EAN-Code erfolgen.



Was ist Competitive Intelligence genau genommen?

In Wikipedia findet sich eine besonders brauchbare Erklärung hierfür. Bedeutsam ist hierbei die Abgrenzung von Competitive Intelligence (CI) zu Business Intelligence (BI) inklusive des Vergleichs von strukturierten (BI) zu unstrukturierten Informationen (CI). Für die Speicherung von unstrukturierten Daten bietet sich hier vorzüglich NoSQL bzw. Elasticsearch [1] - wie in unserem Fall - an.

Auszug aus Wikipedia-Artikel [2]:
»Die Begriffe Competitive Intelligence und Business Intelligence sind zwei unterschiedliche Ansätze, die sich ergänzen. Während Competitive Intelligence überwiegend Daten über Unternehmen analysiert, die außerhalb eines Unternehmens zu finden sind, befasst sich Business Intelligence fast ausschließlich mit der Auswertung firmeninterner Daten. Competitive Intelligence nutzt auch die internen Daten eines Unternehmens, wie etwa Zahlen aus dem Jahresabschluss, die der breiten Öffentlichkeit zur Verfügung gestellt werden. Es bedient sich jedoch überwiegend der unstrukturierten Daten aus dem Internet, oder anderer öffentlicher Informationsquellen. Business Intelligence wiederum basiert fast ausschließlich auf Zahlen aus dem Unternehmen und weniger auf unstrukturierten Daten. Es gibt Bestrebungen auch hier mehr Daten von außerhalb des Unternehmens einzubinden und durch die Einbettung von unstrukturierten Daten, sei es aus dem Unternehmen, oder von außerhalb des Unternehmens, den Zahlen einen Kontext zu verschaffen, kausale Erklärungen für bestimmte Kennzahlen anzubieten.«

Web-Crawling mit Kapow

Es gibt unzählige Tools, die für das Web-Crawling benutzt werden können. Kapow [3] besticht hier vor
allem mit seiner intuitiven Oberfläche, die mit einer kurzen Einarbeitungszeit rasch - für die Umsetzung von Robots - angewendet werden kann. Zusätzlich unterstützt Kapow, im Vergleich zu alternativen Produkten, besonders effektiv JavaScript. Dies ist existentiell, da heutzutage fast jede Web-Seite JavaScript verwendet. Die Workflow-Eigenschaften von Kapow sollten nicht unerwähnt bleiben. Von Projekt zu Projekt muss dann entschieden werden, ob Kapow auch im Bereich Workflow einsetzbar ist. Ein weiterer bedeutender Punkt ist die Normalisierung der Daten. So heißt es „Weißwein“ in einem Shop und in einem anderen „Weisswein“. Mit Kapow ist es – vergleichbar mit einem ETL-Tool von einem Data-Warehouse – machbar, die Daten über die einzelnen Shops hinweg zu normalisieren. Als zusätzliches Beispiel könnte man die Vereinheitlichung von Bewertungen aufführen, die in einem Shop mit 5 Sternen und in einem anderen mit Prozentzahlen angegeben werden.

Warum eine Suchmaschine - wie Elasticsearch - verwenden?

Für den Vergleich von Preisen über unterschiedliche Shops hinweg ist eine eindeutige Kennung notwendig. Hierzu wird bspw. der EAN / GTIN Code benutzt. Die einzelnen Wein-Shops benutzen in unserem Fall allerdings proprietäre Kundennummern. Unter Zuhilfenahme einer Suchmaschine ist es jedoch machbar, die Artikel anhand ihres Artikelnamens - der während der Indizierung eine Trennung der Wörter durchführt - zu finden. Des Weiteren bietet eine Suchmaschine die Möglichkeit, eine Facettierte Suche – wie von Amazon bekannt – bereitzustellen. Eine Facettierte Suche mit blankem SQL aufzubauen ist ein äußerst aufwändiges Unterfangen, da SQL nicht für hierarchische Abfragen entwickelt wurde. Zusätzlich kann eine Suchmaschine wie Elasticsearch auch ausgesprochen umfangreiche Datenmengen (Big Data) verwalten. Auch sollte hierbei nicht vergessen werden, dass dieser Lösungsansatz das Projektbudget im Vergleich zu einer reinen Business-Intelligence-Lösung weniger belastet.

Facettierte Suche

Im nebenstehnden Bild sieht man auf der linken Seite die facettierte Suche. Mit dieser besteht die Möglichkeit
Weine auf Shop-Ebene, Kategorie, Herkunftsland, Anbauregion, Winzer, Preise usw. zu filtern. Zusätzlich ist es denkbar, eine Volltext-Suche bzw. eine Fuzzy-Suche abzusetzen. Mit der Fuzzy-Suche kann man Informationen zu Weinen zu finden, bei denen bspw. der Weinname oder das Weingut falsch geschrieben wurden.

Analysen

Es können verschiedene Analysen durchgeführt werden. Das Beispiel rechts zeigt die Möglichkeit, ein Weingut zu suchen, um zu sehen, bei welchen weiteren Shops dieses zu finden ist. Es werden auch die Weine angezeigt, die ein Shop von einem Weingut führt. Dies kann bspw. interessant sein, wenn ein frischer Katalog geplant wird und man
nicht dieselben Weingüter wie die Konkurrenz führen möchte.

Das Bild rechts veranschaulicht, wie eine grafische Auswertung aussehen könnte. Hierbei wird der prozentuale Anteil von Weinen, die ein
Shop pro Land führt, angezeigt. Diese Grafiken könnten auch für weitere Informationen, wie Visualisierung von Preisspannen etc. dienen. In unserm Fall lässt der Screenshot erkennen, dass  dass bspw. ein Shop (grüner Balken) die meisten deutschen Weine führt. Ein anderer hingegen auf Weine aus Frankreich und Italien führt.

Preisvergleich

Wie oben erwähnt können wir die Preise der unterschiedlichen Shops nicht mit einem eindeutigen Kenner, wie den Barcode (EAN - Code) identifizieren. Hier kommt Elasticsearch ins Spiel. Über ein Groovy-Script werden alle Weine des Ausgangs-Shops mit denen des Wettbewerbs verglichen. Die Ergebnisse dienen zum Aufbau des Preisvergleichs-Index. Des Weiteren haben wir noch das Scoring getuned, z.B. Preise, die um mehr als 50% divergieren, werden ans Ende der Liste gestellt. Schaut man sich das Ergebnis an, so erhält man ein valides Matching von ca. 80% an Weinen, die beim Mitbewerb günstiger bzw. teurer sind. 100% sind nicht zu erreichen, da wir in unserem Fall keinen eindeutigen Kenner haben.

Fazit

Für die Begutachtung werden die Inhalte der Konkurrenz-Web-Seiten in eine Datenbank geladen und im selben Schritt gleichzeitig bereinigt. Für die Analyse wird eine Suchmaschine benutzt, die das Problem eines über alle Web-Shops nicht vorhanden EAN (Barcodes) löst.
Für die Analyse stehen vielfältige Möglichkeiten zur Verfügung:
  • Facettierte Filterung (analog zu Amazon) und Volltextsuche
  • Analysen der Weingüter, Preisspannen, Preisvergleich, Sortimentsverteilung, etc. eines Shops im Vergleich zum Wettbewerb
  • Graphische Darstellungen, bspw. Verteilung der Länder des Sortiments auf die einzelnen Web-Shops, etc.
Neben Elasticsearch wurde für das Analysefrontend Twitter-Bootstrap und AngularJS verwendet.

Links:

[1] http://www.elasticsearch.org http://www.elasticsearch.com
[2] http://de.wikipedia.org/wiki/Competitive_Intelligence
[3] http://www.kapowsoftware.com/de/

Montag, 6. Oktober 2014

Automatische Tests des User Interface einer Web Applikation: Se Builder automatisieren (Teil 6)

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.

Im letzten Teil dieser Reihe will ich demonstrieren, wie man Testläufe im CI Server Jenkinks mit Selenium Grid automatisieren kann.

Beschreibung Testumgebung

Uns steht ein Selenium Grid mit einem Hub und 4 Nodes zur Verfügung. Beim Hub handelt es sich gleichzeitig um unseren Jenkins Master, er hat ein Linux Betriebssystem und dient in unserem Fall nur zur Verteilung der Jobs. Die Nodes laufen unter Windows Vista bzw. Windows 7. Es handelt sich um VMs von modern.ie, jeweils mit einer unterschiedlichen Version des Internet Explorers. Die vier VMs haben die Namen exensiovistaie7, exensiowin7ie8, exensiowin7ie9 und exensiowin7ie10. Bei der zu testenden Applikation handelt es sich um unser Claret Portal. Die Testumgebung ist nur in unserem Intranet verfügbar. Alle VMs sind wie im Teil 5 dieser Blogpost Serie beschrieben vorbereitet. Das bedeutet, alle VMs:
  • sind gestartet
  • es ist unser Standardbenutzer eingeloggt
  • alle Programme im Autostart Verzeichnis des Standardbenutzer wurden ausgeführt
    • Insbesondere also Selenium Grid (mit der Rolle Node)
    • VNC Server
  • haben vorbereitete Einstellungen für den Internet Explorer (identische Sicherheitseinstellungen, Updates deaktiviert, etc.)

Anforderungen

Ich gehe davon aus, dass Testfälle in den meisten Fällen mit dem Selenium Builder aufgezeichnet werden. Danach wird manuell geprüft, ob die so erstellen Testfälle lokal und remote wie erwartet durchlaufen. Laufen die Testfälle stabil, können sie Jenkins übergeben werden. Mit dieser Anforderung werden direkt die mit Selenium Builder erstellen JSON Dateien zum Test herangezogen. Die Anforderung hat den Vorteil, dass kein Export der JSON Dateien erforderlich ist. Möchte man stattdessen z. B. mit Java/JUnit testen, wäre eine Konvertierung des JSON Skripts notwendig. Die erforderliche Export-Funktion bringt Selenium Builder bereits mit. Folgende Exporte sind möglich:
Aktuell sind noch nicht alle Funktionen des Selenium Builders in die Export-Funktion eingebaut. Dadurch kommt es häufig zu Einschränkungen beim Export. Ein relativ simples JSON Skript für den Logout vom Claret Portal führt beispielsweise dazu, dass einige Exportformate nicht mehr möglich sind.

Testfall

Um die Integration von Selenium Builder bzw. Grid in Jenkins zu demonstrieren, habe ich einen relativ einfachen Testfall erstellt. Er besteht aus folgenden Komponenten:
  • JSON Suite test_blogpost_teil_6_suite.json mit den Skripten
    • test_blogpost_teil_6_login.json
    • test_blogpost_teil_6_doSomething.json
      • ruft test_blogpost_teil_6.csv auf
    • test_blogpost_teil_6_logout.json
Der Testfall kann jederzeit mit dem Selenium Builder gestartet und lokal oder remote ausgeführt werden. Der Ablauf beginnt mit dem Einloggen in die zu testende Applikation. Danach wird das „doSomething“ Skript aufgerufen, welches seinerseits die CSV Datei lädt und darin befindliche Parameter einliest. In unserem Fall wird der Parameter retval eingelesen, nachdem anschließend in der Applikation gesucht wird. Am Ende des Testfalls steht der Logout aus der Applikation.

Hinweis CSV Datei: Data-Driven Testing

Selenium Builder bietet die Möglichkeit, externe Variablen einzulesen. Zu finden ist die Funktion unterhalb des Data Menüs. Die Funktion bietet sich an für datenbezogene Tests, wenn eine Reihe von Testfällen mit unterschiedlichen Variablen gefahren werden soll. Zur Verfügung stehen die folgenden Eingabeformate: JSON, XML und CSV:

Ich zeige anhand des folgenden Beipsiel-Skripts, wie auf externe Variablen zugegriffen werden kann und in welcher Form sie eingelesen werden können. Zuerst das Skript für JSON Input Data:
{
  "type": "script",
  "seleniumVersion": "2",
  "formatVersion": 2,
  "steps": [
    {
      "type": "print",
      "text": "${retval}"
    }
  ],
  "data": {
    "configs": {
      "manual": {
        "retval": "exensio"
      },
      "csv": {
        "path": "test_blogpost_teil_6_data.csv"
      }
    },
    "source": "csv"
  },
  "inputs": [
    [
      "retval",
      "string"
    ]
  ],
  "timeoutSeconds": 60
}

Die zugehörige JSON Daten-Datei sieht wie folgt aus:
[ {"retval": "exensio"}, 
  {"retval": "consulting"}, 
  {"retval": "claret"},
  {"retval": "selenium"}
]
Die externen Daten, ein eindimensionales Array, wird als Variable “retval” eingelesen. Im Selenium Builder Skript kann auf die Variable mit ${retval} zugegriffen werden. Auf dieselbe Weise kann man XML- und CSV-Daten verwenden. Die Formate sind wie folgt:
retval
exensio
consulting
claret
selenium
und
<testdata>
<test retval="exensio">
<test retval="consulting">
<test retval="claret">
<test retval="selenium">
</testdata>

Natürlich muss im Selenium Builder Skript jeweils der Dateinamen und der Wert von Source angepasst werden. In den oben gezeigten Fällen haben alle Input Data-Dateien jeweils 4 Werte bzw. Schlüssel-Wert-Paare. Je Wert wird der komplette Testfall einmal durchlaufen. Bei meinem Testfall führt dies also zu vier Durchläufen.

Hinweis WebDriver Capabilities

Das Verhalten des WebDriver (aller Klassen von WebDriver) kann über die sog. Capabilities gesteuert werden. Die wichtigsten Capabilities, speziell für den InternetExplorerDriver sind:
  • browserAttachTimeout [5]
    "Gets or sets the amount of time the driver will attempt to look for a newly launched instance of Internet Explorer."
  • enablePersistentHover [6]
    "Gets or sets a value indicating whether to enable persistently sending WM_MOUSEMOVE messages to the IE window during a mouse hover."
  • ie.forceCreateProcessApi [7]
    "Gets or sets a value indicating whether to force the use of the Windows CreateProcess API when launching Internet Explorer. The default value is false."
  • ie.usePerProcessProxy [8]
    "Gets or sets a value indicating whether to use the supplied Proxy settings on a per-process basis, not updating the system installed proxy setting. This property is only valid when setting a Proxy, where the Kind property is either Direct, System, or Manual, and is otherwise ignored. Defaults to false. "
  • ignoreZoomSetting
    -
  • handlesAlerts [9]
    "Capability name used to indicate whether the browser can handle alerts."
  • nativeEvents [10]
    "Gets or sets a value indicating whether to use native events in interacting with elements."
  • ie.ensureCleanSession [11]
    "Gets or sets a value indicating whether to clear the Internet Explorer cache before launching the browser. When set to true, clears the system cache for all instances of Internet Explorer, even those already running when the driven instance is launched. Defaults to false."
  • elementScrollBehavior [12]
    "Gets or sets the value for describing how elements are scrolled into view in the IE driver. Defaults to scrolling the element to the top of the viewport."
  • ie.browserCommandLineSwitches
    -
  • requireWindowFocus [13]
    "Gets or sets a value indicating whether to require the browser window to have focus before interacting with elements."
  • takesScreenshot [14]
    "Indicates a driver that can capture a screenshot and store it in different ways."
  • javascriptEnabled
    -
  • ignoreProtectedModeSettings
    -
  • enableElementCacheCleanup
    -
  • cssSelectorsEnabled
    -
  • unexpectedAlertBehaviour [15]
    "Determines how unhandled alerts should be handled."
Die Beeinflussung des Verhaltens des Browsers ist mit Vorsicht zu genießen. Manche Optionen sind hilfreich, andere führen zu Abstürzen. Beim Claret Portal kommt das Spring Security Framework zum Einsatz. Mit der Option ie.ensureCleanSession=true möchte ich sicherstellen, dass jeder Testlauf mit leerem Cache startet und auf diese Weise auch der erfolgreiche Login inkl. Cookie und Session getestet wird. Andererseits führte die Verwendung der Option browserAttachTimeout=<integer> zu sofortigen Abstürzen des WebDrivers.

Testaufbau

Für den automatischen Start der Testfälle in Jenkins verwende ich das Tool SeInterpreter [16] in Verbindung mit einem parametrisierten Free Style Job und einem Shell-Aufruf als Buildverfahren. Als Parameter habe ich angelegt:



Eigentlich würden schon die beiden ersten Parameter ausreichen, da ich ja jeweils nur Browser und Version steuern können möchte. Über den Parameter script gebe ich den Pfad zu meiner JSON Suite von Selenium Builder an. Denkbar wäre an der Stelle aber auch den Pfad zu einem Repository zu hinterlegen und somit tatsächliche kontinuierliche Integration zu gewährleisten.

Natürlich erlaube ich in meinem Job die Parallele Ausführung der Builds.
Als Build-Auslöser habe ich die Option "skriptgesteuert" gewählt und dafür einen Token angelegt.
Mit dieser Lösung kann ich später meinen Testfall an unseren DevJob für das Claret Portal knüpfen. Beispielsweise, nachdem das Portal erfolgreich gebaut wurde und alle Unit- und Integrations-Tests durchlaufen wurden, schließen sich die Funktionalen Tests an.
Anstatt des Shell Aufrufs als Buildverfahren wäre auch der Einsatz des Selenium Builder Plugins [17] denkbar. Die Funktionalität dürfte identisch sein.

Mit diesen Einstellungen ist der Testfall bereits vorbereitet für die parallele Ausführung. Für den WebDriver sind parallele Instanzen kein Problem. Allerdings sollte man auf die maximale Anzahl gleichzeitig ablaufender Instanzen auf einem Node achten. Dazu haben wir in Teil 5 der Blogpost Serie die Selenium Grid Nodes mit einer JSON Konfigurationsdatei gestartet.

Zur Verteilung der Testfälle bzw. für den parallelen Start mehrerer Testfälle auf unterschiedlichen Nodes lege ich mir einen weiteren Jenkins Job an, dieses Mal vom Typ Build Flow. Natürlich muss vorher das zugehörige Build Flow Plugin [18] installiert werden. Die Einstellungen dieses Jobs sind sehr simpel, einfach den Flow angeben:
Mit diesen Einstellungen starte ich bei jedem Aufruf des Jobs meinen Testfall fünf Mal parallel. Der Testfall wird mit vier unterschiedlichen Internet Explorer Versionen und einmal mit einem Firefox gestartet. Dabei wird die Firefox Instanz auf einem beliebigen Node ausgeführt. Die IE Instanzen werden parallel auf den jeweiligen Nodes ausgeführt.


Hinweis Stabilität

Die vorgestellte Lösung kann leider nicht als stabil angesehen werden. Trotz einiger Tweaks in Verbindung mit dem InternetExplorerDriver von Selenium kommt es relativ häufig vor, dass sich ein Test aufhängt. Speziell der IE8 bleibt dann scheinbar grundlos stehen bzw. der Test wird nicht sauber beendet. Selbst unter Zuhilfenahme des Build-timeout Plugins [19] gelingt es nicht, das Problem in den Griff zu bekommen. Das Build-timeout Plugin sorgt zwar dafür, dass abgebrochene Jobs auf Jenkins-Seite irgendwann gestoppt und als fehlgeschlagen markiert werden. Es führt aber nicht dazu, dass der laufende Job auf dem Node ebenfalls abgebrochen wird. Je nach Node-Einstellung (Anzahl zu akzeptierten Instanzen), nimmt der Node evtl. keine neuen Jobs mehr an. Das problematische Verhalten des InternetExplorerDrivers bzw. des Internet Explorers selbst ist bekannt. Eine mögliche Lösung könnte von Selenium-Grid-Extras [20] kommen. In dem Projekt wird eine Erweiterung für Selenium Grid entwickelt, die das aktive Beenden aller IE Instanzen auf einem Node ermöglicht. Eine genauere Untersuchung folgt hier in einem neuen Blogpost.

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://blog.exensio.de/2014/09/automatische-tests-des-user-interface.html
[5] https://selenium.googlecode.com/git/docs/api/dotnet/html/P_OpenQA_Selenium_IE_InternetExplorerOptions_BrowserAttachTimeout.htm
[6] https://selenium.googlecode.com/git/docs/api/dotnet/html/P_OpenQA_Selenium_IE_InternetExplorerOptions_EnablePersistentHover.htm
[7] https://selenium.googlecode.com/git/docs/api/dotnet/html/P_OpenQA_Selenium_IE_InternetExplorerOptions_ForceCreateProcessApi.htm
[8] https://selenium.googlecode.com/git/docs/api/dotnet/html/P_OpenQA_Selenium_IE_InternetExplorerOptions_UsePerProcessProxy.htm
[9] https://selenium.googlecode.com/svn/trunk/docs/api/dotnet/html/F_OpenQA_Selenium_Remote_CapabilityType_HandlesAlerts.htm
[10] http://www.nudoq.org/#!/Packages/Selenium.WebDriver/WebDriver/InternetExplorerOptions/P/EnableNativeEvents
[11] https://selenium.googlecode.com/git/docs/api/dotnet/html/P_OpenQA_Selenium_IE_InternetExplorerOptions_EnsureCleanSession.htm
[12] https://selenium.googlecode.com/git/docs/api/dotnet/html/P_OpenQA_Selenium_IE_InternetExplorerOptions_ElementScrollBehavior.htm
[13] http://www.nudoq.org/#!/Packages/Selenium.WebDriver/WebDriver/InternetExplorerOptions/P/RequireWindowFocus
[14] https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/TakesScreenshot.html
[15] https://stackoverflow.com/questions/12913453/how-to-handle-an-alert-with-unexpectedalertbehaviour-capability-in-selenium
[16] https://github.com/sebuilder/se-builder/tree/master/tools/seinterpreter
[17] https://wiki.jenkins-ci.org/display/JENKINS/Selenium+Builder+Plugin
[18] https://wiki.jenkins-ci.org/display/JENKINS/Build+Flow+Plugin
[19] https://wiki.jenkins-ci.org/display/JENKINS/Build-timeout+Plugin
[20] https://github.com/groupon/Selenium-Grid-Extras