Das Listener Interface des Robot Framework

Keine Kommentare

Dem Robot auf die Schrauben geschaut…

Früher oder später kommt sie immer – die Frage nach dem Debugging von Robot. Ob bei einer Vorstellung des Frameworks oder bei einem Training, irgendwann heißt es immer: „Und wie kann man das debuggen?“

Nun könnte man einwenden, eine automatisierte Testsuite sollte man gar nicht debuggen (müssen), schließlich ergibt sich aus den feingranularen Tests der Fehlergrund ganz von selbst. Aber das ist natürlich Illusion. Jeder kennt die Tests, die nur deshalb fehlschlagen, weil ein unerwartetes modales Pop-Up den Fokuswechsel zu einem anderen Fenster nicht zulässt oder weil ein benötigtes Oberflächenelement noch nicht angezeigt wird. Dann wünscht man sich, den Test anhalten zu können, um einfach mal nachzuschauen, was da gerade schief geht.

Und das geht auch mit dem Robot Framework. Die magischen Worte lauten Listener Interface.

Das Robot Framework stellt ein Interface bereit, um als Klassen oder Module implementierte Listener einzubinden. Diese Listener werden beim Start des Frameworks registriert und bekommen dann Nachrichten beim Auftreten einer ganzen Liste von wichtigen Ereignissen. Der Aufruf eines Listeners erfolgt synchron und blockiert die Ausführung der Test-Suite.

Listener können in Java oder in Python implementiert werden. Eine Übersicht aller Ereignisse bzw. aller zulässigen Hooks findet sich im Robot-Userguide. Zum Glück müssen nur die Methoden / Funktionen implementiert werden, die man auch wirklich verwenden möchte.

Implementierte Listener müssen zur Laufzeit im Pfad des Robot Framework liegen, also entweder im Modul-Suchpfad oder durch Verwendung einer Pfadangabe (relativ, absolut) bei der Registrierung.

Zur Registrierung muss ein Listener beim Start des Robot Framework mit der Kommandozeilenoption --listener übergeben werden. Dabei können auch Argumente an den Listener übergeben werden. Dazu später mehr.

Ein einfacher Listener

Beginnen wir mit einem konkreten Beispiel: Wir möchten, dass der Testlauf vor Beginn eines neuen Testfalls gestoppt wird und erst nach dem Klick auf einen Button in einem Dialogfenster weiterläuft.

Hier zunächst unsere sehr komplexe und total realistische Test-Suite:

Dateiname: EinfacherTest.txt

*** Test Case ***
Einfacher Testfall
  [Documentation]  Ein einfacher Testfall als Listener-Demo
  ${test_var}=  Set Variable  OK
  Should Be Equal  ${test_var}  OK

Nun brauchen wir einen Listener, der zu Beginn eines neuen Testfalls einen Dialog öffnet und auf einen Klick wartet. Dazu bauen wir uns ein Python-Modul, das die Funktion start_test implementiert. start_test hat zwei Parameter, den Namen des Testfalls als String und die Attribute des Testfalls als Dictionary. Welche Attribute in dem Dictionary übergeben werden, steht im Userguide.

Dateiname: BitteWarten.py

import tkMessageBox
from Tkinter import Tk
 
ROBOT_LISTENER_API_VERSION = 2
 
def start_test(name, attributes):
    Tk().withdraw() # Root-Fenster entfernen
    tkMessageBox.showinfo("Please click 'OK'...",
                          "Test Case Name: " + name)

Nun können wir den Listener beim Start von Robot übergeben und uns das Ergebnis anschauen.

pybot --listener BitteWarten EinfacherTest.txt

Als Ergebnis sollte folgender Dialog dargestellt werden:

Listener mit Argumenten

Nun ist dieser Ansatz spätestens dann viel zu einfach, wenn man mehr als einen Testfall in der Suite hat. Keiner möchte 20mal „Ok“ klicken, um bis zum gewünschten Testfall vorzudringen. Hier helfen Listener weiter, die man mittels Argumenten konfiguriert.

In Python muss man die Implementierung von Modul auf Klasse umstellen, wenn man Listener mit Argumenten verwenden möchte, da die Argumente im Konstruktor übergeben werden. Also muss die Listener-Klasse eine __init__-Methode implemenentieren, die alle erwarteten Parameter in der richtigen Reihenfolge entgegennimmt. In unserem Fall benötigen wir nur einen Parameter, den Testnamen.

Anmerkung: Da Argumente optional sind, sollten die Parameter der __init__-Methode immer sinnvolle Default-Werte haben.

Dateiname: BitteWartenMitArg.py

import tkMessageBox
from Tkinter import Tk
 
class BitteWartenMitArg():
    ROBOT_LISTENER_API_VERSION = 2
 
    def __init__(self, test_name = ''):
        self.test_name = test_name
 
    def start_test(self, name, attributes):
        if (name == self.test_name) or ('' == self.test_name):
            Tk().withdraw() # Root-Fenster entfernen
            tkMessageBox.showinfo("Please click 'OK'...",
                                  "About to start test '%s'" % name)

Dieser Listener wartet vor dem Start des angegebenen Testfalls bzw. bei allen Testfällen, falls kein Testfallname übergeben wurde.

Um die Funktionsweise wirklich zu prüfen, brauchen wir natürlich einen weiteren Testfall für unsere Test-Suite…

Dateiname: ZweiTests.txt

*** Test Cases ***
Einfacher Testfall
  [Documentation]  Ein einfacher Testfall als Listener-Demo
  ${test_var}=  Set Variable  OK
  Should Be Equal  ${test_var}  OK
 
Weiterer Test
  [Documentation]  Don't be evil...
  Log  Nur keine Panik!

Bei der Registrierung des Listeners werden Argumente durch Doppelpunkte getrennt angegeben. Weitere Argumente werden mit zusätzlichen Doppelpunkten angehängt.

pybot --listener BitteWartenMitArg:"Einfacher Testfall" ZweiTests.txt

Wenn alles richtig läuft, wird die Testausführung nur vor dem ersten Testfall angehalten, aber nicht vor dem zweiten Testfall.

Hinweis: (Doppelte) Anführungszeichen werden hier nur verwendet, weil das Argument ein Leerzeichen enthält. Grundsätzlich brauchen Argumente nicht in Anführungszeichen gesetzt zu werden.

Jeder sollte auch mal ausprobieren was passiert, wenn man das Argument weglässt.

Variablen mittels Listener anzeigen

Aber Listener können auch noch mehr. So können sie z.B. auch die Test-Libraries verwenden. Über die „BuiltIn“-Library können somit Variablen ausgelesen werden.

Dateiname: VariableAnzeigen.py

import tkMessageBox
from Tkinter import Tk
from robot.libraries.BuiltIn import BuiltIn
 
ROBOT_LISTENER_API_VERSION = 2
 
def end_test(name, attributes):
    Tk().withdraw() # Root-Fenster entfernen
    tkMessageBox.showinfo("Please click 'OK'...",
                          "test_var = '%s'" % BuiltIn().get_variables()['${test_var}'])

Die Ausführung mit…

pybot --listener VariableAnzeigen EinfacherTest.txt

… sollte folgende Ansicht ergeben:

Und wie geht’s jetzt weiter?

Diese Beispiele sollten zeigen, dass man mit Listenern im Robot Framework einiges anstellen kann. Mit Sicherheit hat sich jemand bereits vor vier Absätzen gedacht, „Hey, damit kann man ja bestimmt ganz einfach einen echten Debugger bauen!“ Bevor sich jetzt jeder vor die Konsole klemmt und ein neues Repository in GitHub anlegt: Gibt’s schon und zwar hier. Über die Verwendung dieses Remote-Debuggers für Robot werden ich demnächst ein paar Zeilen schreiben.

 

Exkurs „API-Version“

Vielleicht ist bereits die Variable ROBOT_LISTENER_API_VERSION in meinen Beispielen aufgefallen. Durch sie wird festgelegt, welche Version der Listener-API der Listener verwenden möchte. Die API ist mit Robot Framework Version 2.1 heftig überarbeitet worden. Um die Kompatibilität alter Listener zu gewährleisten, wurde die Variable ROBOT_LISTENER_API_VERSION eingeführt. Ist sie auf den Wert 2 gesetzt, so wird die neue Version der API verwendet. Default-Verhalten beim Fehlen dieser Variable ist die Verwendung der alten API-Version. Es gibt keinen Grund, die alte API-Version bei neuem Code noch zu verwenden (man müsste außerden im Repository erstmal eine alte Userguide-Version suchen, die die API-Version 1 beschreibt…), aber dieses Default-Verhalten kann zu Verwirrung führen, gerade wenn man nicht täglich mit Python arbeitet oder wieder einmal neuen Code mittels Copy & Paste „schreibt“…

Vergisst man dabei nämlich die Angabe der API-Version oder stellt sie aus Versehen außerhalb der Klassendefinition, so wird der Listener mit der alten API angesprochen und man bekommt die tollsten Fehlermeldungen. Also, wenn man Fehlermeldungen im Stile von

[ ERROR ] Calling listener method 'start_test' of listener 'BitteWartenMitArg' failed: TypeError: start_test() takes exactly 3 arguments (4 given)

bekommt, einfach mal nachschauen, ob ROBOT_LISTENER_API_VERSION vorhanden ist, den richtigen Wert 2 hat und auch an der richtigen Stelle steht. In diesem Fall hatte ich die Variable vor class BitteWartenMitArg(): verschoben und somit aus der Klassendefinition entfernt – ein typischer Copy-Paste-Fehler.

 

Exkurs „Java“

Ganz zu Anfang habe ich geschrieben, dass Listener auch in Java geschrieben werden können. Aber wie sieht das genau aus und was ist dabei zu beachten?

Hier zunächst die drei Listener, in Java umgesetzt…

Dateiname: BitteWartenJ.java

import java.util.Map;
import javax.swing.JOptionPane;
 
public class BitteWartenJ {
    public static final int ROBOT_LISTENER_API_VERSION = 2;
 
    public void startTest(String name, Map attributes) {
        JOptionPane.showMessageDialog(null, "Test name: " + name);
    }
}

Dateiname: BitteWartenMitArgJ.java

import java.util.Map;
import javax.swing.JOptionPane;
 
public class BitteWartenMitArgJ {
    public static final int ROBOT_LISTENER_API_VERSION = 2;
 
    private String testName;
 
    // Leerer Konstruktor, falls das Argument weggelassen wird...
    public BitteWartenMitArgJ() {
        this.testName = "";
    }
 
    // Konstruktor, der das Argument entgegennimmt...
    public BitteWartenMitArgJ(String testName) {
        this.testName = testName;
    }
 
    public void startTest(String name, Map attributes) {
        if (name.equals(testName) || ("".equals(testName)))
            JOptionPane.showMessageDialog(null, "Test name: " + name);
    }
}

Dateiname: VariableAnzeigenJ.java

import java.util.Map;
import javax.swing.JOptionPane;
 
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
 
public class VariableAnzeigenJ {
    public static final int ROBOT_LISTENER_API_VERSION = 2;
 
    public void endTest(String name, Map attributes) throws ScriptException {
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("python");
        engine.eval("from robot.libraries.BuiltIn import BuiltIn");
        engine.eval("v = BuiltIn().get_variables()['${test_var}']");
        Object testVar = engine.get("v");
        JOptionPane.showMessageDialog(null, "test_var = " + testVar);
    }
}

Grundsätzlich kann man erstmal feststellen: Es geht tatsächlich! Allerdings sind die Java-basierten Listener bei dem geringen funktionalen Umfang dieser Beispiele doch recht umständlich in der Handhabung.

Folgende Punkte verdienen Beachtung:

  • Natürlich muss Robot in diesem Fall mit Jython (jybot) gestartet werden.
  • Extensions werden bei der Registierung eines Listener ignoriert. Bie der gleichzeitigen Verwendung von Python und Java kann es daher passieren, dass man versehentlich Modul- / Klassen-Namen doppelt verwendet. Robot lädt die Python-Variante.
  • Im Gegensatz zu Python-Code muss der Java-Code vorher kompiliert werden.
  • Argumente sind optional! Daher braucht man auch Konstruktoren, bei denen Argumente „fehlen“ (siehe mittleres Beispiel).
  • Der Zugriff auf Python-basierte Libraries ist etwas umständlicher (siehe letztes Beispiel).

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.