Aus der QA-Küche 2: Beliebte Probleme mit Selenium und Jenkins

Wie im vorangegangenen Beitrag möchte ich hier zwar nichts weltbewegendes, jedoch nützliches zu Tests via Selenium für Einsteiger zum Besten geben. Heute im Automations-Kontext via Jenkins CI Server. Wie das funktioniert, dazu finden sich etliche Blog-Einträge, die aber überwiegend mehr oder weniger das gleiche (ab-)schreiben. Hier noch ein paar zusätzliche Kniffe:
Selenium html-Report
Um diesen fesch per Jenkins darzustellen (siehe Bildschirmfoto), am besten das Jenkins Seleniumhq-Plug-in installieren und in der Job-Konfiguration ein Häkchen bei „Publish Selenium Report“ setzen. In die Zeile „Test report HTMLs“ kommt dann der Workspace-relative Pfad samt Dateiname, in meinem Fall also einfach selenium.html (vgl. unten Option bash-Aufruf des Selenium-Servers).
Selenium aufrufen
Wir benötigen Selenium sowie Xvfb. Beides führe ich nicht als Plug-in aus, sondern bastel’ die Aufrufe für letzteres sowie einen Selenium-Standalone-Server mit ins bash script, simpel und ergreifend, da die Tests ohnehin einige Befehle zur Vorbereitung benötigen (d.h. in meinem speziellen Fall für icinga-web, führe ich daher hier nicht auf) und ich sowohl die Abfolge der Kommandos als auch Selenium-Parameter bequemer festlegen kann.

  • Wir fahren zunächst den Selenium server per URL herunter (falls dieser aufgrund von abgebrochenen Jobs o.Ä. noch ungewollter Weise läuft):
curl -f --connect-timeout 3 http://build.icinga.org:4444/selenium-server/driver/?cmd=shutDownSeleniumServer || :

Jenkins bricht bei Fehlermeldungen (also, wenn Selenium nicht läuft) nicht ab, da wir Error codes per || : ins naheligende Nirvana schubsen. > /dev/null 2>&1 und die „Klappe halten“-curl-Option -f reichen dafür nicht aus.

  • Selbiges funktioniert ebenso grandios mit Xvfb:
/etc/init.d/xvfb stop > /dev/null 2>&1 || :
/etc/init.d/xvfb start > /dev/null 2>&1 || :
  • Wir legen dann einen Monitor fest:
export DISPLAY=":666"
  • Und rufen den Seleniume-Server auf:
java -jar ./selenium-server-standalone-2.0.0.jar -debug -htmlSuite "*googlechrome /opt/google/chrome/chrome" http://wir.sind.dieb.org/ /jenkins/selenium/test-suite.html selenium.html -timeout 3600 -userExtensions ./selenium/user-extensions.js -avoidProxy -browserSessionReuse

Erläuterung
Achten Sie generell darauf, wohin Sie die Optionen und Parameter schreiben und wie. Teilweise bekommen Sie bei Fehlern keine Fehlermeldung, der Test läuft aber nicht wie erwartet oder funktioniert gar nicht.

  • “*googlechrome /opt/google/chrome/chrome” (bitte mit den neckischen “)
    sorgt dafür, dass der Browser direkt aufgerufen wird. Somit vermeiden wir nachstehende Fehlermeldung, die man nicht einfach ignorieren sollte, da andernfalls einige Events nicht korrekt erkannt werden:

Caution: ‘/usr/bin/firefox’: file is a script file, not a real executable. The browser environment is no longer fully under RC control

  • -timeout 3600
    Es gibt bei Selenium zwei verschiedene Arten von Timeouts: Der setTimeout-Befehl (respektive selenium.defaultTimeout in der Konfiguration) setzt die Zeit in ms fest, bis ein Kommando (waitFor, open) zu lange dauert und fehlschlägt.
    Die Startoptiontimeout, gibt wiederum in Sekunden vor, wie lange, der gesamte Testablauf dauern darf, bevor abgebrochen wird. 3600 Sekunden, das wäre eine Stunde, was inzwischen ernsthaft knapp wird. Falls Ihre Test Suite zu viel Zeit in Anspruch nimmt und abgebrochen wird, erhalten Sie zum einen lediglich eine leere Report-Datei, zum anderen eine Fehlermeldung der Art:

WARN – Google Chrome seems to have ended on its own.
HTML suite exception seen:
org.openqa.selenium.server.SeleniumCommandTimedOutException

  • -debug
    Sie bekommen einfach ein deutlich besseres Console-Protokoll in Jenkins angezeigt. Ansonsten sehen Sie zwischen Start und Ende von Selenium einfach keine Rückmeldung ob des Fortschritts.

xvfb Einstellungen

  • konfigurieren Sie den export-Bildschirm mit 24 bit – 16 oder gar 8 bit bereiten diverse Probleme. In meinem Fall habe ich die Optionen direkt in /etc/init.d/xvfb mit folgender Zeile verankert:
case "$1" in
 start)
 /usr/bin/Xvfb :99 -ac -screen 0 1280x1024x24 &
 ;;
  • Sollte es mit Xvfb Probleme geben bzw. Sie den start- oder export-Befehl vergessen haben, ist dies meist Ursache der folgenden Fehlermeldung:

java.lang.RuntimeException: Timed out waiting for profile to be created!

Entgegen häufiger Empfehlung brachte das an- sowie festlegen eines Firefox-Profils bei mir keine Abhilfe.

  • Ignorieren können Sie hingegen Meldungen von Xvfb wie

Could not init font path element /usr/share/fonts/X11/100dpi/:unscaled, removing from list!

Screenshots erstellen
Wer einen kurzen Blick in die Selenium-Dokumentation wirft, wird zwar schnell feststellen, dass Screenshots und html-Suite nur unter Verwendung des Firefox im Chrome-Modus vereinbar sind, allerdings bleibt unerwähnt, dass es unter Umständen auch dort Probleme bereitet. Exemplarisch ein Aufruf, wie er bei mir in Selenium IDE vorkommt:

captureEntirePageScreenshot
 //tmp/screen-001a.png
  • Lassen Sie die Screenshots im tmp-Ordner speichern, das verursacht gewöhnlich keine Probleme; abschließend lassen Sie Jenkins einfach die Bilder in den Workspace-Ordner verschieben, so haben Sie bequem Zugriff darauf.
  • Die Screenshot-Funktion schlägt bei „nicht ordentlichen“ html-Seiten fehl, also wenn diese beispielsweise nicht/fehlerhaft geladen wurde oder es sich um reinen Text oder formloses xml handelt.
  • Für etwas wie „Sceenshot on fail“ wäre mir bei html-Suites keine Möglichkeit bekannt. Machen Sie Screenshots daher immer am Anfang des nächsten Tests bzw. legen Sie einen extra Fall an, der dies nachträglich macht. Schlägt ein Test fehl, springt Selenium zum nächsten Testfall, der im Idealfall ein Bild vom Schlamassel anfertigt.
  • Ohnehin möchte ich hier nochmals daran erinnern, bestimmte Teile der Testfälle immer „umgekehrt“ anzuordnen, jedenfalls neigte ich bei meinen ersten Schritten dazu, sklavisch an dem sich ergebenden zeitlichen Ablauf zu hängen, was grundverkehrt ist. Es ist falsch, Fenster zu schließen oder sonstige Änderungen während – oder zum Abschluss eines Tests rückgängig zu machen. Schlägt dieser fehl, werden die nötigen Schritte natürlich nicht ausgeführt, man erhält keine Rückmeldung und schlimmstenfalls geraten nachfolgende Prüfungen ins Straucheln.

Weekly Snap: Icinga Training, Testing with Selenium & Apprenticeships

13 – 17 February offered something from every corner of the office, from new apprenticeship openings and Icinga training courses to programming and time management tips.
Bernd began by calling out to aspiring apprentices to join our consulting and managed services teams.
Ansgar then tested Icinga Web with the help of Selenium IDE to deal with dynamic IDs and Gunnar shared his newest programming recommendation – Lua.
From the events team, Markus announced our next Icinga monitoring course in Dusseldorf on 19 – 23 March, while Pamela reminded early birds to hurry for their tickets to the OSDC on 25 -26 April.
Following on, Ronny alerted sys admins to dkim filters to a recent segfault error that has arisen in combination with libmilter 1.0.1 and versions before 8.14.4-1.
Tobias took on the issue of time management and IT work to close the week.

Aus der QA-Küche: Selenium IDE an jung-dynamischen IDs

Inzwischen habe ich die wichtigsten Teile von icinga-web erfasst und indiziert (ich erwähnte diesen Vorgang bereits beiläufig) und sogar die meisten der nötigen Testfälle sind be- und geschrieben, wenngleich diese hie- und da Nachbesserungen und Erweiterungen erfahren werden. Den Index selbst halte ich allerdings für schwer verdauliche Kost für Blog-Einträge. Stattdessen serviere ich ein paar Grundlagen im Umgang mit Selenium IDE mit ausgewählten Problemen der Saison, die sich nicht schnell genug auf die Bäume retten konnten.
Ein kurzer Blick auf icinga-web verrät uns: ein paar Eigenheiten machen es einem nicht leicht, automatisierte Tests durchzuführen. Als da wären zu nennen:

  • Recht umfangreiche, dynamische Struktur mit nur einigen wenigen fest vergebenen IDs (mehr für zukünftige Versionen geplant).
  • Zeitkritische, Performance-abhängige Vorgänge (Datenbankzugriff, Authentifizierung)
  • Im Hintergrund (die per Selenium natürlich angesprochen werden können) befindliche, je nach Nutzerrechten variierende und durch Plug-Ins erweiterbare Elemente

Die Basis: Ich hab da mal was vorbereitet …
Einigen Komplikationen lassen sich durch regelmäßiges Zurücksetzen des Benutzerprofils, kurz gehaltenen (ggf. in mehrere Abschnitte aufgeteilte) Testfällen, löschen der Cookies und möglichst wenig geöffneten Modulen vorbeugen. Als Grundlage dient Icinga mit einer leicht modifizierten Testkonfiguration, etwa mit zugefügten Custom Variables und geänderten „Flapping“-Einstellungen, um nachvollziehbare Werte zu erhalten. Neue Commits erkennt und installiert Jenkins CI automatisch, initialisiert die Datenbank neu, entfernt Überbleibsel wie Logs, Cachedateien etc. und startet schließlich der Selenium Server mit unserer Test-Suite.
Ja, wo laufen sie denn – Verwirrung (vor)programmiert
Trotz aller Vorsichtsmaßnahmen besteht permanent Verwechslungs- und „Nicht-Wiedererkennungs“-Gefahr von Elementen, egal ob nun per ID, xPath, Text oder gar css-Pfad Identifiziert. Da ist es immer wieder erfrischend, was bei vermeintlich unfehlbaren, simplen Click&Verify-Prüfungen alles schief gehen kann. Für die Vorspeise nehmen wir (also das „Kinderarzt-wir“) uns einen Harmlosigkeit heuchelnden „OK“-Knopf zur Brust, den wir (ver)drücken wollen. Bei der Aufzeichnung ermittelt Selenium IDE kurz und bündig:
//div/div/div/div[2]/div/table/tbody/tr/td[2]/table/tbody/tr/td/table/tbody/tr/td/table/tbody/tr[2]/td[2]/em/button
Das ist zum einen weder beim durchsehen der Test-Cases selbst als auch dem Testprotokoll sonderlich aussagekräftig. Zum anderen könnten schon kleinen Änderungen an unserem Test oder der Software eine Korrektur des Pfads nötig machen. Also gestalten wir das ganze ein wenig „unschärfer“, nach dem Motto: So genau wie nötig, so ungenau wie möglich (Hoho! Aufgemerkt – als Lehrer wäre ich große Klasse im Dumme-Lehrsprüche-klopfen …). Mit Firebug den Code betrachtet, kommen wir auf
<button id=”ext-gen225″ type=”button” style=”background-color: transparent;”>OK</button>
wobei die id=”ext-gen225″ wie erwähnt lustig wechselt. Dagegen lässt sich die class selbst, wenn der Knopf irgendwo anders in der Struktur landet, mit Selenium finden. Als Target sieht das so aus:
//*/em/button[contains(@class, ‘x-btn-text icinga-action-icon-ok’)]
contains ist in diesem speziellen Fall streng genommen nicht erforderlich. Es ist der exakte Wert und verändert sich nicht. Ebenso könnte man sich /em/button sparen. Es dient der Veranschaulichung. Ganz banal könnten wir auch nach dem Text des Knopfes (oder eines Links) fahnden (beides targets):
link=OK
oder
//*[.=’OK’]
Trotz allem versagen diese zwei Varianten, sobald ein weiteres Knopf-Pendant mit den gesuchten Attributen vorhanden ist (was des öfteren vorkommt) und wir würden immer das in der Struktur weiter oben angesiedelte „OK“ erwischen.
Direkt ist besser
Bei Funktionen, die lediglich Mittel zum Zweck sind (d.h. nicht einen vorhandenen Dialog, Knopf etc. direkt prüfen soll), etwa „Ausloggen“, können wir zwar einen irgendwo im Code vorhandenen Link via target
//a[contains(@href, ‘/icinga-web/modules/appkit/logout?logout=1’)]
ausfindig machen und ansprechen. Narrensicher ist es dagegen, die URL direkt aufzurufen oder Seleniums deleteAllVisibleCookies dafür zu missbrauchen. Seite anschließend neu laden, fertig.
Hin und wieder leistet bei dem zwar bequemen, aber eingeschränkten Selenium IDE ein eingestreuter JavaScript-Befehl gute Dienste, wie letztlich im Code der Seite zu finden. Hier ein Beispiel, ob die Zeitmarken der gewählten Zeitzone entsprechen:

Command Target Value
store javascript{new Date().getHours()}  localtime
verifyText //td/div/div/div  *${localtime}*



Oder hier ein direkter Aufruf der Sucheingabe und setzten des Fokus darauf:

Command Target Value
runScript AppKit.search.SearchHandler.getSearchbox().showSearch();
runScript AppKit.search.SearchHandler.getSearchbox().focus();

Alles zu seiner Zeit
Selenium haut die Befehle teils so schnell raus, dass es bei diversen Gelegenheiten wie Markieren, Abhaken, Tastatureingaben und anschließendem Speichern von Einstellungen zu Geschmacksverirrungen kommen kann. Zwar kann es fruchten, die Ausführungsgeschwindigkeit mit setSpeed für bestimmte Abschnitte herabzusetzen oder Pausen zu setzen. Die bessere Lösung ist es jedoch für Gewöhnlich, anderweitig zu sichern, dass die gewünschten Elemente, Eingaben etc. vorhanden sind. Denn wie langsam es laufen muss und darf, ist ein Schätzwert, der je nach Serverauslastung auch mal schwanken kann. Zudem läuft nach einem Fehlschlag womöglich der Rest der Test ebenfalls nur sehr langsam weiter.
Ob ein Element vorhanden ist, genügt oft nicht als Kriterium, beispielsweise, wenn sich dieses im Hintergrund befindet, wie die Globale Suche. Neben Befehlen wie waitForVisible bieten sich als Alternative oft Werte wie attributes, values, positions … zu Prüfung an, die sich bei dem Vorgang ändern. Wir lassen Selenium erst lostippen, wenn das Suchfeld unserer Aufmerksamkeit gewiss ist:

Command Target Value
waitForAttribute name=global_search@class x-form-text x-form-field x-form-focus

Oder lassen warten, bis die Daten unseres Test-Users tatsächlich geladen sind und in den Feldern erscheinen:

Command Target Value
waitForValue name=user_name test

Da alle fünf Minuten die Authentifizierung erneuert wird, kann es vorkommen, dass in einem ungünstigen Moment gesendete Kommandos fehlschlagen. Dem wirken wir mit einem per flowControl-Erweiterung zubereiteten Nachtisch entgegen und senden bei einem Fehlschlag einfach erneut (und drücken die Daumen, nicht ewig der Schleife zu hängen ;)):

Command Target Value
label retrysend
clickAt css=div.x-grid3-row-checker 1,1
clickAt //button[.=&quot;Commands&quot;] 1,1
waitForVisible //*[contains(@class, ‘x-menu-item-icon icinga-icon-comment’)]
clickAt //*[contains(@class, ‘x-menu-item-icon icinga-icon-comment’)] 1,1
waitForTextPresent persistent
type name=comment testcomment
clickAt name=comment 10,10
clickAt //*[contains(@class,’x-btn-text icinga-icon-accept’)] 10,10
storeText //div[2]/div/div/div/div/div/div[2]/span
while storedVars.fail == ‘Authentification failed’
click //*[.=’OK’;]
goto retrysend
endWhile