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

von Roland Rickborn

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:

<script>
    $('.image0ad67c52-ada2-4a25-9042-cf4d4ce10393').tooltipsy({content: 'Achtung'});
</script>

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.

Kategorien: SeleniumTesting

Zurück