Handbuch

12
Skripting

Es ist einer der großen Vorteile von QF-Test, dass komplexe Tests erstellt werden können, ohne eine einzige Zeile Code zu schreiben. Allerdings gibt es Dinge, die sich mit einem GUI alleine nicht bewerkstelligen lassen. Für ein Programm, das Daten in eine Datenbank schreibt, könnte es z.B. sinnvoll sein, zu überprüfen, ob die Daten korrekt geschrieben wurden. Oder man könnte Testdaten aus einer Datenbank oder einer Datei lesen und mit diesen einen Test ausführen. All das und mehr wird mit Hilfe der mächtigen Skriptsprachen Jython, Groovy und JavaScript ermöglicht.

4.2+Jython ist von Anfang an dabei, Groovy seit QF-Test Version 3. Ab Version 4.2 kann man auch JavaScript als Skriptsprache verwenden. Es ist eine Frage des Geschmacks, welcher dieser Sprachen man den Vorzug gibt. Wer jedoch bereits mit Java vertraut ist, wird sich wahrscheinlich eher mit Groovy denn mit Jython anfreunden. Web Entwickler werden vermutlich JavaScript verwenden.

In diesem Kapitel werden zunächst die Grundlagen der Skriptintegration und die in allen Skriptsprachen zur Verfügung stehenden Module beschrieben. Auf die Besonderheiten der Sprachen Groovy Skripting, Jython Skripting und JavaScript Skripting wird in den jeweiligen Abschnitten eingegangen.

3.0+ Die Skriptsprache eines Knotens wird nun mit dem Attribut 'Skriptsprache' eines 'Server Skript' oder 'SUT Skript' Knotens festgelegt. Somit können also alle drei Sprachen innerhalb einer Testsuite parallel verwendet werden. Welche Sprache als Standard verwendet werden soll, kann über die Option Voreingestellte Sprache für Skript Knoten eingestellt werden.

12.1
Allgemeines

Beim Skripting ist die Herangehensweise von QF-Test genau umgekehrt zu der anderer GUI Testprogramme. Anstatt den gesamten Test durch ein Skript zu steuern, bettet QF-Test kleine Skripte in die Testsuite ein. Dies geschieht mit Hilfe der Knoten 'Server Skript' und 'SUT Skript'.

Beiden Knoten gemeinsam ist das Attribut 'Skript' für den eigentlichen Programmcode.

Detailansicht eines Server Skript Knotens mit Hilfefenster
Abbildung 12.1:  Detailansicht eines 'Server Skript' Knotens mit Hilfefenster für rc-Methoden

3.0+ Der in QF-Test integrierte Skripteditor verfügt über ein paar nützliche Eigenschaften, die das Eingeben des Codes erleichtern. Reservierte Schlüsselwörter, eingebaute Funktionen, Standard-Objekttypen, Literale und Kommentare werden farblich hervorgehoben. Innerhalb von Blöcken werden Codezeilen automatisch eingerückt und am Blockende wieder ausgerückt. Mit Hilfe von [TAB] können auch mehrere markierte Zeilen von Hand ein- oder ausgerückt ([Shift-TAB]) werden.

Das vielleicht - zumindest für den QF-Test Neuling - wichtigste Feature des integrierten Editors ist jedoch die Eingabehilfe für viele eingebaute Methoden. Gibt man beispielsweise rc. ein (und ggf. zusätzlich einen oder mehrere Anfangsbuchstaben eines Methodennamens) und drückt dann [Ctrl-Leertaste], so erscheint ein Popup-Fenster mit den passenden Runcontext Methoden und ihrer Beschreibung (vgl. Kapitel 44). Nach Auswahl einer Methode und anschließender Bestätigung mit [Eingabe] wird die gewählte Methode in den Skriptcode eingefügt. Drückt man [Ctrl-Leertaste] nach einem Leerzeichen, wird eine Liste aller Objekte angezeigt, für die Hilfe zur Verfügung steht.

'Server Skripte' sind für Dinge wie das Berechnen von Variablenwerten oder das Einlesen und Parsen von Testdaten nützlich. 'SUT Skripte' öffnen dagegen den unbeschränkten Zugang zu den Komponenten des SUT und zu allen anderen Java Schnittstellen, die das SUT bietet. Ein 'SUT Skript' könnte z.B. zum Auslesen oder Überprüfen von Werten im SUT verwendet werden, auf die QF-Test keinen Zugriff hat. Im 'SUT Skript' Knoten muss das Attribut 'Client' auf den Namen des SUT Clients gesetzt sein, in dem es ausgeführt werden soll.

'Server Skripte' werden in jeder Skriptsprache jeweils in einem Interpreter ausgeführt, der in QF-Test selbst integriert ist, während 'SUT Skripte' in jeweils einem im SUT integrierten Interpreter laufen. Diese Interpreter sind voneinander unabhängig und haben keine gemeinsamen Zustände. QF-Test nutzt die RMI Verbindung zum SUT für eine nahtlose Integration der 'SUT Skripte' in die Testausführung.

Über die Menüeinträge »Extras«-»Jython Terminal...«, »Extras«-»Groovy Terminal...« etc. können Sie ein Fenster mit einer interaktiven Kommandozeile für den in QF-Test eingebetteten Interpreter öffnen. Darin können Sie mit der jeweiligen Skriptsprache experimentieren, um ein Gefühl für die Sprache zu entwickeln, aber auch komplexe Dinge ausprobieren wie z.B. das Herstellen der Verbindung zu einer Datenbank. Mittels [Strg-Hoch] und [Strg-Runter] können Sie frühere Eingaben wieder verwenden. Außerdem können Sie beliebige Zeilen im Terminal bearbeiten oder eine Region markieren und mittels [Return] an den Interpreter schicken. Dabei filtert QF-Test die vom Interpreter stammenden '>>>' und '...' Markierungen heraus.

Entsprechende Terminals gibt es auch für SUT Clients. Diese Terminals sind über das »Clients« Menü zugänglich.

Hinweis Wenn Sie in einem SUT Skripting Terminal arbeiten, müssen Sie eines beachten: Die Kommandos werden vom Interpreter nicht im Event Dispatch Thread ausgeführt, im Gegensatz zu Kommandos, die in einem 'SUT Skripten' Knoten ausgeführt werden. Das sagt Ihnen möglicherweise nichts und meistens stellt es auch kein Problem dar, aber wenn Sie auf Swing oder SWT Komponenten zugreifen oder deren Methoden aufrufen, besteht die Gefahr, dass die gesamte Applikation einfriert. Um das zu verhindern stellt QF-Test die globale Funktion runAWT (bzw. runSWT) zur Verfügung, mit deren Hilfe Sie beliebigen Code im Dispatch Thread ausführen können. Um zum Beispiel die Anzahl der sichtbaren Knoten einer JTree Komponente namens tree zu ermitteln, verwenden Sie runAWT("tree.getRowCount()") (bzw. runAWT { tree.getRowCount() } in Groovy) um ganz sicher zu gehen.

12.2
Der Runcontext rc

Zur Ausführung von 'Server Skripten' und 'SUT Skripten' stellt QF-Test eine spezielle Umgebung zur Verfügung, zu der u.a. das Runcontext Objekt gehört, das den aktuellen Zustand der Ausführung eines Tests repräsentiert. Auf dieses Objekt kann über die Variable "rc", welche in allen Sprachen verfügbar ist, zugegriffen werden. Es bietet Schnittstellen (vollständig dokumentiert in Abschnitt 44.6) für den Zugriff auf QF-Test Variablen, zum Aufruf von QF-Test 'Prozeduren' und um Meldungen in das Protokoll zu schreiben. Ein 'SUT Skript' kann mit seiner Hilfe außerdem auf die echten Java Komponenten des GUI im SUT zugreifen.

Für Fälle, in denen kein Runcontext verfügbar ist, z.B. Resolver, TestRunListener, Code der in einem Hintergrund-Thread ausgeführt wird etc. bietet QF-Test ein Modul namens qf mit hilfreichen generischen Methoden zum Logging und für andere Zwecke an. Detaillierte Informationen hierzu finden Sie in Abschnitt 44.7.

Weiterführende Beispiele finden Sie in der Testsuite doc/tutorial/demo-script.qft. Diese Beispiele sind in den verschiedenen Skriptsprachen verfügbar.

12.2.1
Meldungen ausgeben

Ein Einsatzgebiet des Runcontexts ist die Ausgabe beliebiger Meldungen im Protokoll, das QF-Test für jeden Testlauf erstellt. Diese Meldungen können auch als Warnungen oder Fehler markiert werden.

rc.logMessage("This is a plain message")
rc.logWarning("This is a warning")
rc.logError("This is an error")
Beispiel 12.1:  Meldungen aus Skripten ausgeben

Wird - wie empfohlen - mit kompakten Protokollen gearbeitet (vgl. die Option Kompakte Protokolle erstellen), Kompakte Protokolle erstellen), werden Knoten, die aller Wahrscheinlichkeit nach nicht für eine Fehleranalyse benötigt werden, eventuell aus dem Protokoll entfernt um Speicher zu sparen. Dies betrifft nicht die Fehlermeldung (rc.logError). Hier wird immer die Meldung selbst und etwa 100 vorhergehende Knoten im Protokoll aufgehoben. Bei einer Warnung rc.logWarning wird auf jeden Fall die Warnung behalten, jedoch keine vorhergehenden Knoten. Normale Meldungen (rc.logMessage) werden gegebenenfalls entfernt. Wenn Sie eine normale Meldung zwingend im Protokoll behalten wollen, können Sie dies über den optionalen zweiten Parameter (dontcompactify) erreichen:

rc.logMessage("This message will not be removed", dontcompactify=true)
rc.logMessage("This message will not be removed", 1)
Beispiel 12.2:  Meldungen, die nicht aus kompakten Protokollen entfernt werden
12.2.2
Checks durchführen

Die Ausgabe einer Meldung ist meist an eine Bedingung geknüpft. Außerdem ist es oft wünschenswert, im XML oder HTML Report ein Ergebnis analog zu einem 'Check' Knoten zu erhalten. Hierzu dienen die Methoden rc.check und rc.checkEqual:

x = 0
rc.check(x == 0, "Value of x is 0")
userlang = rc.lookup("system", "user.language")
rc.checkEqual(userlang, "en", "English locale required",
              rc.EXCEPTION)
Beispiel 12.3:  Checks durchführen

Das optionale letzte Argument legt die Fehlerstufe fest. Hierbei können rc.EXCEPTION, rc.ERROR, rc.OK bzw. rc.WARNING verwendet werden.

12.2.3
Variablen

In QF-Test gibt es verschiedene Arten von Variablen. Es wird einerseits unterschieden zwischen QF-Test Variablen und Variablen der Skriptsprachen. Die Variablen der Skriptsprachen wiederum werden unterteilt in Server- und SUT-seitige Variablen des jeweiligen Interpreters. Die folgende Grafik verdeutlicht die Sichtbarkeit der jeweiligen Variablen Arten:

Übersicht über die Variablen
Abbildung 12.2:  Übersicht über die verschiedenen Variablen in QF-Test

Um in den Skripten mit diesen unterschiedlichen Variablen zu arbeiten und dieses auszutauschen, stellt der Runcontext spezielle Methoden zur Verfügung. Diese Methoden werden in den nachfolgenden Abschnitten erläutert.

12.2.3.1
Zugriff auf Variablen

Auf Variablen von QF-Test in einem Skript zuzugreifen ist nicht weiter schwierig. Auf Textvariablen können Sie mittels der Runcontext Methode lookup (siehe Abschnitt 44.6 für API Beschreibung) zugreifen.

# access a simple variable
text = rc.lookup("someText")
# access a property or resource
version = rc.lookup("qftest", "version")
Beispiel 12.4:  Zugriff auf Textvariablen mittels rc.lookup
12.2.3.2
Variablen setzen

Um die Ergebnisse eines Skripts für die weitere Ausführung eines Tests bekannt zu machen, können Werte in globalen oder lokalen QF-Test Variablen abgelegt werden. Der Effekt entspricht der Ausführung eines 'Variable setzen' Knotens. Die entsprechenden Methoden im Runcontext sind rc.setGlobal und rc.setLocal.

# Test if the file /tmp/somefile exists
from java.io import File
rc.setGlobal("fileExists", File("/tmp/somefile").exists())
Beispiel 12.5:  Verwendung von rc.setGlobal

Nach Ausführung des obigen Skripts wird $(fileExists) in einem Knoten von QF-Test zu 'true' expandieren, wenn die Datei /tmp/somefile existiert und zu 'false', wenn sie nicht existiert.

Um eine Variable zu löschen, setzen Sie deren Wert auf None in Jython bzw. null in Groovy und JavaScript . Mittels rc.clearGlobals() aus einem 'Server Skript' können alle globalen Variablen gelöscht werden.

12.2.3.3
Globale Variablen

Manchmal ist es hilfreich, eine Skript Variable in verschiedenen Skriptknoten der gleichen Sprache zur Verfügung zu haben. Falls der Wert der Variablen kein simpler String oder Integer ist, genügt es nicht, diese mit setGlobal(...) als globale QF-Test Variable zu definieren, da der Wert dadurch in einen String umgewandelt wird. Stattdessen sollten Sie die Variable als global deklarieren, wie es das folgende Beispiel zeigt.

global globalVar
globalVar = 10000
Beispiel 12.6:  Globale Jython Variable

globalVar steht nun in allen folgenden Skriptknoten desselben Typs zur Verfügung ('Server Skripte' oder 'SUT Skripte' desselben Clients). Um den Wert von globalVar in einem anderen Skriptknoten zu verändern, ist erneut eine Deklaration mit dem Schlüsselwort global notwendig. Andernfalls wird eine neue lokale Variable mit gleichem Namen erzeugt. Um eine globale Jython Variable zu entfernen, kann die del Anweisung verwendet werden:

global globalVar
del globalVar
Beispiel 12.7:  Löschen einer globalen Jython Variable

In Groovy und JavaScript werden globale Variablen noch einfacher erzeugt als in Jython. Die Regel lautet, dass undeklarierte Variablen im Binding des Skripts erwartet werden. Sind sie dort nicht zu finden, werden sie automatisch hinzugefügt.

	
myGlobal = 'global'
Beispiel 12.8:  Definieren von globalen Variablen in Groovy bzw. JavaScript
assert myGlobal == 'global'
def globals = binding.variables
assert globals['myGlobal'] == 'global'
globals.remove('myGlobal')
assert globals.find { it == 'myGlobal' } == null
Beispiel 12.9:  Verwenden und entfernen einer globalen Groovy Variablen
12.2.3.4
Austausch von Variablen zwischen verschiedenen Interpretern

Es kommt vor, dass Variablen, die in einem Interpreter definiert wurden, später in einem anderen Interpreter benötigt werden. So könnte zum Beispiel eine Liste von Werten, die mit Hilfe eines 'SUT Skripts' aus einer Tabelle gelesen werden, in einem 'Server Skript' weiterverwendet werden, um darüber zu iterieren.

Um derartige Aufgaben zu vereinfachen, stellt der Runcontext einen symmetrischen Satz von Methoden zum Zugriff auf und zur Modifikation von Variablen in einem anderen Skript Interpreter bereit. Für 'SUT Skripte' sind dies die Methoden toServer und fromServer. Die entsprechenden Methoden für 'Server Skripte' heißen toSUT und fromSUT.

Das folgende Jython Beispiel zeigt, wie ein 'SUT Skript' direkt eine globale Variable im Jython Interpreter von QF-Test setzen kann:

cellValues = []
table = rc.lookup("idOfTable")
for i in range(table.getRowCount()):
    cellValues.append(table.getValueAt(i, 0))
rc.toServer(tableCells=cellValues)
Beispiel 12.10:  Setzen einer Server Variablen aus einem 'SUT Skript' heraus

Nach Ausführung des obigen Skripts enthält die globale Variable namens "tableCells" in QF-Test's Jython Interpreter das Array der Werte aus der Tabelle.

Hinweis Die Tabellenwerte im obigen Beispiel sind nicht notwendigerweise Strings. Sie könnten Zahlen sein, Datumswerte, was auch immer. Leider ist der pickle Mechanismus von Jython nicht mächtig genug, um Instanzen von Java Klassen zu transportieren (nicht einmal von serialisierbaren), so dass der Austauschmechanismus auf primitive Typen wie Strings und Zahlen sowie auf Jython Objekte und Strukturen wie Arrays und Dictionaries beschränkt ist.

12.2.4
Zugriff auf die GUI Komponenten des SUT

Für 'SUT Skripte' bietet der Runcontext eine äußerst nützliche Methode. Durch den Aufruf von rc.getComponent("componentId") werden die Informationen aus dem 'Komponente' Knoten mit der 'QF-Test ID' "componentId" aus der Testsuite geholt und an den Mechanismus zur Wiedererkennung von Komponenten gereicht. Dieser arbeitet genau wie bei der Simulation eines Events, das heißt, er wirft auch die entsprechenden Exceptions, falls die Komponente nicht gefunden werden kann.

Im Erfolgsfall wird die Komponente an das Skript zurückgegeben und zwar nicht in Form von abstrakten Daten, sondern das konkrete Objekt. Alle Methoden, die die Java API der Klasse dieser Komponente zur Verfügung stellt, können ausgeführt werden, um Informationen auszulesen oder um Effekte zu erzielen, die durch das GUI nicht möglich sind. Um eine Liste der Methoden einer Komponente anzuzeigen, siehe Abschnitt 5.5.

# get the custom password field
field = rc.getComponent("tfPassword")
# read its crypted value
passwd = field.getCryptedText()
rc.setGlobal("passwd", passwd)
# get the table component
table = rc.getComponent("tabAddresses")
# get the number of rows
rows = table.getRowCount()
rc.setGlobal("tableRows", rows)
Beispiel 12.11:  Zugriff auf Komponenten mit rc.getComponent

Sie können auf diesem Weg auch auf Unterelemente zugreifen. Wenn der Parameter componentId ein Element referenziert, liefert getComponent ein Paar zurück, bestehend aus der Komponente und dem Index des Elements. Der Index kann dazu verwendet werden, den eigentlichen Wert zu ermitteln. Das folgende Beispiel zeigt, wie Sie den Wert einer Tabellenzelle auslesen. Beachten Sie dabei auch die praktische Methode mit der Jython das Auspacken von Sequenzen bei Zuweisungen unterstützt.

# first get the table and index
table, (row,column) = rc.getComponent("tableAddresses@Name@Greg")
# then get the value of the table cell
cell = table.getValueAt(row, column)
Beispiel 12.12:  Zugriff auf Unterelemente mit rc.getComponent
12.2.5
Aufruf von 'Prozeduren'

Der Runcontext kann auch dazu verwendet werden, 'Prozeduren' in QF-Test auszuführen.

rc.callProcedure("text.clearField",
         {"component" : "nameField", "message" : "nameField cleared"})
Beispiel 12.13:  Einfacher Prozeduraufruf in Jython

In obigem Beispiel wird die 'Prozedur' namens "clearField" im 'Package' namens "text" aufgerufen. Die Parameter für den Aufruf sind "component" mit dem Wert "nameField" und "message" mit dem Wert "nameField cleared".

Dasselbe Beispiel mit der veränderten Groovy Syntax:

rc.callProcedure("text.clearField",
         ["component" : "nameField", "message" : "nameField cleared"])
Beispiel 12.14:  Einfacher Prozeduraufruf in Groovy

Und in JavaScript:

rc.callProcedure("text.clearField",
         {"component" : "nameField", "message" : "nameField cleared"})
Beispiel 12.15:  Einfacher Prozeduraufruf in JavaScript

Der Rückgabewert einer 'Prozedur', der mittels eines 'Return' Knotens festgelegt werden kann, ist gleichzeitig der Rückgabewert des rc.callProcedure Aufrufs.

Hinweis In einem 'SUT Skript' Knoten sollte rc.callProcedure(...) nur mit großer Vorsicht verwendet werden. Rufen Sie nur 'Prozeduren' mit kurzer Laufzeit auf, die keine allzu komplexen Operationen im SUT auslösen. Andernfalls könnte eine DeadlockTimeoutException verursacht werden. Wenn Daten für datengetriebene Tests zwingend im SUT ermittelt werden müssen, transferieren Sie diese mittels rc.toServer(...) zu QF-Test's Interpreter und treiben Sie die Tests dann aus einem 'Server Skript' Knoten, für den es keine derartigen Einschränkungen gibt.

3.1+12.2.6
Setzen von Optionen

Viele der in Kapitel 36 beschriebenen Optionen können auch zur Laufzeit via rc.setOption gesetzt werden. Konstanten für die Namen dieser Optionen sind in der Klasse Options definiert, welche in den Skriptsprachen automatisch verfügbar ist.

Ein reelles Beispiel, bei dem es sinnvoll ist, eine Option temporär zu setzen, ist die Wiedergabe eines Events auf eine deaktivierte Komponente. Für diesen Sonderfall muss die Überprüfung durch QF-Test auf den enabled/disabled Zustand verhindert werden:

rc.setOption(Options.OPT_PLAY_THROW_DISABLED_EXCEPTION, false)
Beispiel 12.16:  setOption

Nach abspielen des speziellen Events sollte der ursprüngliche Wert der Option, wie er aus der Konfigurationsdatei gelesen oder im Optionen-Dialog gesetzt wurde, wieder hergestellt werden, wie im folgendem Beispiel gezeigt:

rc.unsetOption(Options.OPT_PLAY_THROW_DISABLED_EXCEPTION)
Beispiel 12.17:  unsetOption

HinweisStellen Sie sicher, dass Sie QF-Test Optionen immer in einem 'Server Skript' Knoten und SUT Optionen in einem 'SUT Skript' Knoten setzen, andernfalls hat die Aktion keinen Effekt. Die Dokumentation der Optionen in Kapitel 36 führt für jede Option den korrekten Knoten auf.

12.2.7
Komponenten bei Bedarf setzen

Es könnten Fälle auftreten in denen Sie eine bestimmte Komponente suchen müssen, um mit dieser arbeiten zu können. Manchmal kann das Aufzeichnen aller in Frage kommenden Komponenten sehr mühsam oder schlicht zu kompliziert sein. Für solche Fälle können Sie die Methode rc.overrideElement verwenden, um die gefundene Komponente (Suche mittels generischen Komponenten oder mittels Skript) einer QF-Test Komponente zuzuordnen. Danach können Sie mit den gewohnten QF-Test Knoten mit dieser Komponente arbeiten.

Stellen Sie sich vor, wir möchten immer mit dem ersten Textfeld eines Panels arbeiten. Jedoch könnte das einfache Aufzeichnen der Textfelder nicht möglich sein, da sich der Inhalt zu stark ändert. Nun können wir ein Skript implementieren, welches das erste Textfeld sucht. Dann können wir dieses gefundene Textfeld einer Komponente PriorityAwtSwingComponent aus der Standardbibliothek qfs.qft zuordnen. Nachdem wir das Skript ausgeführt haben, können mit der Angabe der QF-Test ID PriorityAwtSwingComponent alle gewohnten QF-Test Knoten benutzen um mit dem gefundenen Textfeld zu arbeiten.

panel = rc.getComponent("myPanel")
for component in panel.getComponents():
  if qf.isInstance(component, "javax.swing.JTextField"):
     rc.overrideElement("PriorityAwtSwingComponent", component)
     break
          
Beispiel 12.18:  Jython rc.overrideElement

Dieses Konzept ist sehr nützlich, wenn Sie einen Algorithmus kennen, um ihre Zielkomponenten für bestimmte Testschritte zu suchen.

Sie können solche Priority-Komponenten für alle unterstützten Engines in der Standardbibliothek qfs.qft finden. Ein Beispiel finden Sie auch in Ihrer QF-Test Installation in der mitgelieferten Testsuite carconfig_de.qft im Verzeichnis demo/carconfig.

12.3
Jython Skripting

Python ist eine vielseitige, objektorientierte Skriptsprache, die von Guido van Rossum entworfen und in C implementiert wurde. Hilfreiche Informationen zu Python gibt es unter http://www.python.org. Python ist eine standardisierte Sprache und seit vielen Jahren etabliert. Umfassende Dokumentation dazu ist frei verfügbar, daher beschränkt sich dieses Handbuch darauf, die Integration von Jython in QF-Test zu erklären. Die Sprache selbst ist sehr natürlich und intuitiv. Ihre größte Stärke ist die Verständlichkeit und Lesbarkeit von Python Skripten. Daher sollten Sie keine Probleme haben, die folgenden Beispiele zu verstehen.

Jython (früher JPython genannt) ist eine Implementierung der Programmiersprache Python in Java. Jython hat dieselbe Syntax wie Python und verfügt über beinahe identische Features. Die Objektsysteme von Java und Jython haben vieles gemeinsam und Jython kann nahtlos in Anwendungen wie QF-Test integriert werden. Das macht es zu einem äußerst nützlichen Werkzeug für Java Skripting. Jython hat seine eigene Homepage unter http://www.jython.org. Dort gibt es unter anderem auch ein ausführliches Tutorial zum Einstieg.

QF-Test verwendet die Jython Version 2.7, die einen Großteil der Standard Python Bibliothek unterstützt.

HinweisIn Jython werden QF-Test Variablen der Form $(var) oder ${Gruppe:Name} vor Ausführung des Skripts expandiert. Dies kann zu unerwünschten Effekten führen. Daher wird geraten, die Methode rc.lookup() (vgl. Abschnitt 12.2.3.1) zu verwenden, die während der Ausführung des Skripts evaluiert wird.

12.3.1
Module

Module für Jython in QF-Test sind nichts anderes als gewöhnliche Python Module. Sie können Module in QF-Test importieren und deren Methoden aufrufen, was die Entwicklung komplexer Skripte stark vereinfacht und außerdem die Wartbarkeit Ihrer Tests erhöht, da Module testsuiteübergreifend verfügbar sind.

Module, die Sie für mehrere Testsuiten zur Verfügung stellen wollen, sollten Sie im jython Verzeichnis unter QF-Tests Wurzelverzeichnis ablegen. Module, die speziell für eine Testsuite geschrieben sind, können auch direkt im selben Verzeichnis wie die Testsuite liegen. Das versionsspezifische Verzeichnis qftest-4.4.1/jython/Lib ist für Module von Quality First Software GmbH reserviert. Jython Module haben die Endung .py.

Das folgende Beispiel zeigt ein Jython Modul, das eine Prozedur zur Verfügung stellt, die eine Liste von Zahlen sortiert:

def insertionSort(alist):
    for index in range(1,len(alist)):

        currentvalue = alist[index]
        position = index

        while position>0 and alist[position-1]>currentvalue:
            alist[position]=alist[position-1]
            position = position-1

        alist[position]=currentvalue
Beispiel 12.19:  The Jython module pysort.py

Das folgende Jython Skript ruft die im Modul definierte Prozedur auf.

import pysort

alist = [54,26,93,17,77,31,44,55,20]
pysort.insertionSort(alist)
print(alist)
Beispiel 12.20:  Jython script using a module
12.3.2
Post-mortem Fehleranalyse von Jython Skripten

In Python gibt es einen einfachen zeilenorientierten Debugger namens pdb. Zu seinen nützlichen Features gehört die Möglichkeit zu analysieren, warum ein Skript mit einer Exception fehlgeschlagen ist. In Python können Sie hierzu einfach nach einer Exception das pdb Modul importieren und pdb.pm() ausführen. Damit gelangen Sie in eine Debugger-Umgebung in der Sie die Werte der Variablen zum Zeitpunkt des Fehlers betrachten und auch den Call-Stack hinauf navigieren können um dort weitere Variablen zu analysieren. Das Ganze ist vergleichbar mit der Analyse eines Core-Dump einer C Anwendung.

Obwohl Jython den pdb Debugger grundsätzlich unterstützt, funktioniert er aus verschiedenen Gründen in QF-Test nicht besonders gut, aber immerhin ist die post-mortem Analyse von Skripts über die Jython Terminals möglich. Nach einem fehlgeschlagenen 'Server Skript' Knoten öffnen Sie QF-Test's Jython Terminal, für ein gescheitertes 'SUT Skript' das Jython Terminal des entsprechenden SUT, und geben dort einfach debug() ein. Dies sollte denselben Effekt wie das oben beschriebene pdb.pm() haben. Weitere Informationen zum Python Debugger entnehmen Sie bitte der Dokumentation unter https://docs.python.org/2/library/pdb.html.

12.3.3
Boolean Typ

Jython hat einen echten Boolean Typ mit den Werten True und False. In älteren Versionen dienten die Integer Werte 0 und 1 als Boolean Werte. Dies kann zu Problemen führen, wenn das Ergebnis eines Aufrufs wie file.exists() einer QF-Test Variable zugewiesen wird, z.B. "fileExists", und später in einem 'Bedingung' Attribut in der Form $(fileExists) == 1 ausgewertet wird. Derartige Bedingungen sollten grundsätzlich in der einfachen Form $(fileExists) geschrieben werden, die mit allen Jython Versionen funktioniert.

12.3.4
Zeichenkodierung

Alle Java Strings sind Sequenzen von 16-Bit Zeichen. In Python basierten zunächst alle Strings auf 8-Bit Zeichen, später kamen 16-Bit Unicode Strings hinzu. In Jython direkt in Skripten angegebene Strings wie "abc" sind 8-Bit Strings, durch Voranstellen eines 'u' wie bei u"abc" erhält man 16-Bit Unicode Strings.

In Jython 2.2 wurden Java Strings in 8-Bit Python Strings konvertiert, basierend auf der Standard-Zeichenkodierung der Java VM, in der westlichen Hemisphäre üblicherweise ISO-8859-1 (auch als latin-1 bekannt). In Jython 2.5 werden Java Strings nun grundsätzlich als Unicode Jython Strings interpretiert. Dies kann an einigen Stellen durch implizite Konvertierung zwischen 8-Bit und 16-Bit zu Problemen führen, z.B. wenn ein - nun als Unicode interpretierter - Java String und ein direkt angegebener String verknüpft werden, wie in rc.lookup("path") + "/file". Oft geht dies gut, doch wenn der direkte String Zeichen außerhalb des 7-Bit ASCII-Zeichensatzes enthält, wird es unangenehm. Die Standard-Kodierung für 8-Bit Zeichen in Jython kann über die Option Standard-Zeichenkodierung für Jython festgelegt werden. Standard für QF-Test ist latin-1 für ein Maximum an Rückwärtskompatibilität. Ein Vorteil dieser Änderungen ist, dass nun auch andere Standard-Kodierungen möglich sind und so direkte Strings mit Zeichen aus internationalen Zeichensätzen in Jython Skripten verwendet werden können.

Eine Sache auf die Sie aufpassen müssen ist bestehender Code der Form

import types
if type(somevar) == types.StringType:
    ...

Der Typ types.StringType bezeichnet nur 8-Bit Strings, keine Unicode Strings. Um zu testen, ob eine Variable ein Jython String ist, egal ob 8-Bit oder Unicode, ändern Sie den Code in

import types
if type(somevar) in types.StringTypes:
    ...

Eine neue Anforderung an Python Module stammt aus neueren Python Versionen: Wenn das Modul Zeichen außerhalb des 7-Bit ASCII-Zeichensatzes enthält, muss die Zeichenkodierung für das Modul zwingend in einem Kommentar im Kopf der Datei angegeben sein, z.B.

# coding: latin-1

Weitere Informationen hierzu finden Sie unter http://www.python.org/peps/pep-0263.html.

12.3.5
Den Namen einer Java Klasse ermitteln

Diese einfache Operation ist in Jython überraschend schwierig. Bei einem gegebenen Java Objekt würde man den Namen der Klasse einfach mittels obj.getClass().getName() bestimmen. Für manche Objekte funktioniert das auch in Jython, für andere scheitert es mit einer kryptischen Fehlermeldung, was recht frustrierend sein kann. Es geht immer dann schief, wenn die Klasse selbst auch eine getName Methode implementiert. Dies ist für AWT Component der Fall, so dass es für alle AWT/Swing Komponenten schwierig ist, den Namen ihrer Klasse zu ermitteln.

Die einzige Lösung, die zuverlässig funktioniert ist:

from java.lang import Class
Class.getName(obj.getClass())

Da der Code nicht gerade intuitiv ist, haben wir ein neues Modul namens qf mit praktischen Methoden initiiert. Es ist automatisch verfügbar, so dass Sie nun einfach folgendes schreiben können:

qf.getClassName(obj).

12.3.6
Ein komplexes Beispiel

Wir schließen diesen Abschnitt mit einem komplexen Beispiel ab, das Features von Jython und QF-Test kombiniert, um einen datengetriebenen Test durchzuführen. Wir gehen für dieses Beispiel von einer einfachen Tabelle mit den drei Spalten "Name", "Age" und "Address" aus, die mit Werten gefüllt werden soll, die aus einer Datei gelesen werden. Die Datei soll dabei im "Comma-Separated-Values" Format vorliegen, mit '|' als Trennzeichen, eine Zeile pro Tabellenzeile, z.B.:

John Smith|45|Some street, some town
Julia Black|35|Another street, same town

Das Beispiel testet die Funktionalität des SUT neue Tabellenzeilen zu erstellen. Dabei kommt eine QF-Test Prozedur zum Einsatz, die 3 Parameter erwartet - "name", "age" und "address" - und mit diesen eine neue Tabellenzelle anlegt und füllt. Im Jython SUT Skript wird die Datei mit den Werten eingelesen und geparst. In einer Schleife wird über die Datensätze iteriert und für jede zu erstellende Tabellenzeile die Prozedur aufgerufen. Der Name für die Datei wird in der QF-Test Variable namens "filename" übergeben. Wenn das Füllen der Tabelle abgeschlossen ist, wird der Endzustand der Tabelle mit den eingelesenen Werten verglichen, um sicher zu gehen, dass alles geklappt hat.

import string
data = []
# read the data from the file
fd = open(rc.lookup("filename"), "r")
line = fd.readline()
while line:
    # remove whitespace
    line = string.strip(line)
    # split the line into separate fields
    # and add them to the data array
    if len(line) > 0:
        data.append(string.split(line, "|"))
    line = fd.readline()

# now iterate over the rows
for row in data:
    # call a qftest procedure to create
    # one new table row
    rc.callProcedure("table.createRow",
                     {"name": row[0], "age": row[1],
                      "address": row[2]})

# verify that the table-rows have been filled correctly
table = rc.getComponent("tabAddresses")

# check the number of rows
if table.getRowCount() != len(data):
    rc.logError("Row count mismatch")
else:
    # check each row
    for i in range(len(data)):
        if str(table.getValueAt(i, 0)) != data[i][0]:
            rc.logError("Name mismatch in row " + str(i))
        if str(table.getValueAt(i, 1)) != data[i][1]:
            rc.logError("Age mismatch in row " + str(i))
        if str(table.getValueAt(i, 2)) != data[i][2]:
            rc.logError("Address mismatch in row " + str(i))
Beispiel 12.21:  Ein datengetriebener Test

Natürlich dient obiges Beispiel nur zur Anschauung. Es ist viel zu komplex, um halbwegs komfortabel in QF-Test editiert werden zu können. Außerdem sind zu viele Dinge fest verdrahtet, so dass es mit der Wiederverwendbarkeit nicht weit her ist. Für eine echte Anwendung würde man den Code zum Einlesen und Parsen der Datei parametrisieren und in ein Modul auslagern, ebenso den Code zur Verifikation der Tabelle.

Dies geschieht im folgenden Jython Skript mit den Methoden loadTable zum Lesen der Daten aus der Datei und verifyTable zum Überprüfen der Tabelle. Es wird in einem Modul namens csvtable.py abgespeichert. Ein Beispiel dafür finden Sie in qftest-4.4.1/doc/tutorial/csvtable.py. Zur Erläuterung genügt folgende vereinfachte Version:

import string

def loadTable(file, separator="|"):
    data = []
    fd = open(file, "r")
    line = fd.readline()
    while line:
        line = string.strip(line)
        if len(line) > 0:
            data.append(string.split(line,separator))
        line = fd.readline()
    return data

def verifyTable(rc, table, data):
    ret = 1
    # check the number of rows
    if table.getRowCount() != len(data):
        if rc:
            rc.logError("Row count mismatch")
        return 0
    # check each row
    for i in range(len(data)):
        row = data[i]
        # check the number of columns
        if table.getModel().getColumnCount() != len(row):
            if rc:
                rc.logError("Column count mismatch " +
                            "in row " + str(i))
            ret = 0
        else:
            # check each cell
            for j in range(len(row)):
                val = table.getModel().getValueAt(i, j)
                if str(val) != row[j]:
                    if rc:
                        rc.logError("Mismatch in row " +
                                    str(i) + " column " +
                                    str(j))
                    ret = 0
    return ret
Beispiel 12.22:  Schreiben eines Moduls

Der obige Code sollte Ihnen bekannt vorkommen. Er ist eine verbesserte Version von Teilen von Beispiel 12.21. Ist dieses Modul installiert, vereinfacht sich der Code, der in QF-Test geschrieben werden muss, wie folgt:

import csvtable
# load the data
data = csvtable.loadTable(rc.lookup("filename"))
# now iterate over the rows
for row in data:
    # call a qftest procedure to create
    # one new table row
    rc.callProcedure("table.createRow",
                     {"name": row[0], "age": row[1],
                      "address": row[2]})

# verify that the table-rows have been filled correctly
table = rc.getComponent("tabAddresses")
csvtable.verifyTable(rc, table, data)
Beispiel 12.23:  Aufruf von Methoden in einem Modul
12.4
Groovy Skripting

Groovy ist eine weitere etablierte Skriptsprache für die Java Platform. Sie wurde von James Strachan and Bob McWhirter im Jahre 2003 entwickelt. Im Grunde ist alles was man für Groovy braucht, eine Java Laufzeitumgebung (JRE) und die Datei groovy-all.jar. Diese Bibliothek enthält sowohl einen Compiler, um Java Class Dateien zu erstellen, wie auch die entsprechende Laufzeitumgebung, um diese Klassen in der Java Virtual Machine (JVM) auszuführen. Man kann sagen, Groovy ist Java mit einer zusätzlichen .jar Datei. Im Gegensatz zu Java ist Groovy allerdings eine dynamische Sprache, was bedeutet, dass das Verhalten von Objekten erst zur Laufzeit ermittelt wird. Außerdem können Klassen auch direkt aus dem Skriptcode geladen werden, ohne erst Class-Dateien erzeugen zu müssen. Schließlich lässt sich Groovy auch leicht in Java Anwendungen wie QF-Test einbetten.

Die Groovy Syntax ist ähnlich der von Java, vielleicht ausdrucksstärker und leichter zu lesen. Wenn man von Java kommt, kann man sich dem Groovy Stil nach und nach annähern. Wir können hier natürlich nicht die Sprache Groovy in allen Details besprechen, dazu sei auf die Groovy Homepage http://groovy-lang.org/ oder das exzellente Buch "Groovy in Aktion" von Dierk Koenig u.a. verwiesen. Vielleicht können aber die folgenden Hinweise einem Java Programmierer beim Einstieg in Groovy helfen.

  • Das Semikolon ist optional, solange eine Zeile nur ein Statement enthält.
  • Klammern sind manchmal optional, zum Beispiel bedeutet println 'hello qfs' dasselbe wie println('hello qfs').
  • Anstelle von for (int i = 0; i < len; i++) { ... } verwende man for (i in 0..<len) { ... }.
  • Die folgenden Importe werden bei Groovy standardmäßig vorgenommen: java.lang.*, java.util.*, java.io.*, java.net.*, groovy.lang.*, groovy.util.*, java.math.BigInteger, java.math.BigDecimal.
  • Alles ist ein Objekt, sogar Integer oder Boolean Werte wie '1' oder 'true'.
  • Anstelle von Getter- und Setter-Methoden wie obj.getXxx() kann man einfach obj.xxx verwenden.
  • Der Operator == prüft auf Gleichheit statt auf Identität, so dass Sie if (somevar == "somestring") statt if (somevar.equals("somestring")) verwenden können. Um auf Identität zu prüfen, gibt es die Methode is().
  • Variablen haben einen dynamischen Typ, wenn sie mit dem Schlüsselwort def deklariert werden. def x = 1 zum Beispiel erlaubt es, der Variablen x später auch einen String zuzuweisen.
  • Arrays werden etwas anders als in Java definiert, z. B. int[] a = [1, 2, 3] oder def a = [1, 2, 3] as int[]. Mit def a = [1, 2, 3] wird in Groovy eine Liste definiert.
  • Groovy erweitert die Java Bibliothek indem für viele Klassen zusätzliche Methoden definiert werden. So kann in einem Groovy-Skript etwa die Methode isInteger() auf ein String Objekt angewendet werden. Diese Erweiterungen werden als GDK bezeichnet (analog zu JDK in Java). Eine Liste der GDK-Methoden für ein Objekt obj liefert der Ausdruck obj.class.metaClass.metaMethods.name oder - übersichtlicher - das folgende Beispiel:
import groovy.inspect.Inspector

def s = 'abc'
def inspector = new Inspector(s)
def mm = inspector.getMetaMethods().toList().sort() {
    it[Inspector.MEMBER_NAME_IDX] }
for (m in mm) {
    println(m[Inspector.MEMBER_TYPE_IDX] + ' ' +
            m[Inspector.MEMBER_NAME_IDX] +
            '(' + m[Inspector.MEMBER_PARAMS_IDX] + ')')
}
Beispiel 12.24:  GDK-Methoden für ein String Objekt
  • Innere Klassen werden nicht unterstützt. In den meisten Fällen können stattdessen Closures verwendet werden. Eine Closure ist ein Object, das einen Code-Schnipsel repräsentiert. Sie kann Parameter haben und auch ein Wert zurückliefern. Genau wie ein Block wird eine Closure in geschweiften Klammern definiert. Blöcke gibt es nur im Zusammenhang mit class, interface, statischer oder Objekt-Initialisierung, Methodenrümpfen, if, else, synchronized, for, while, switch, try, catch und finally. Jedes andere Vorkommen von {...} ist eine Closure. Als Beispiel schauen wir uns die GDK-Methode eachFileMatch der Klasse File an. Sie hat zwei Parameter: einen Filter (z. B. ein Pattern Objekt) und eine Closure. Diese Closure hat selbst auch einen Parameter: ein File Object, das die gerade gefundene Datei repräsentiert.
def dir = rc.lookup('qftest', 'suite.dir')
def pattern = ~/.*\.qft/
def files = []
new File(dir).eachFileMatch(pattern) { file ->
    files.add(file.name)
}
files.each {
    // Auf ein einzelnes Closure-Argument kann mit "it" zugegriffen werden.
    rc.logMessage(it)
}
Beispiel 12.25:  Closures
  • Mit Listen (List) und Dictionaries (Map) lässt es sich in Groovy viel leichter arbeiten als in Java.
def myList = [1, 2, 3]
assert myList.size() == 3
assert myList[0] == 1
myList.add(4)

def myMap = [a:1, b:2, c:3]
assert myMap['a'] == 1
myMap.each {
    this.println it.value
}
Beispiel 12.26:  Listen und Dictionaries
12.4.1
Groovy Packages

Genau wie Java Klassen werden Groovy Skriptdateien (.groovy) in Packages organisiert. Diejenigen, welche suiteübergreifend Anwendung finden, stellt man am besten in den groovy Ordner unterhalb des QF-Test Wurzelverzeichnisses. Dateien bzw. Packages, die speziell für eine Testsuite entwickelt worden sind, können auch im Verzeichnis der Testsuite abgelegt werden. Das versionsspezifische Verzeichnis qftest-4.4.1/groovy ist für Groovy-Dateien reserviert, die von Quality First Software GmbH bereitgestellt werden.

package my

class MyModule
{
    public static int add(int a, int b)
    {
        return a + b
    }
}
Beispiel 12.27:  MyModule.groovy

Die Datei MyModule.groovy könnte etwa im Unterverzeichnis my unterhalb des Testsuite-Verzeichnisses abgespeichert werden. Die Methode add aus MyModule kann dann folgendermaßen aufgerufen werden:

import my.MyModule as MyLib

assert MyLib.add(2, 3) == 5
Beispiel 12.28:  Using MyModule

Dieses Beispiel demonstriert gleichzeitig noch ein weiteres Groovy Feature: Type Aliasing. Indem import und as zusammen verwendet werden, kann man eine Klasse über einen Namen eigener Wahl referenzieren.

12.5
JavaScript Skripting

JavaScript hat sich vor allem im Bereich der Webentwicklung durchgesetzt und ist dort eine sehr beliebte Programmiersprache. QF-Test unterstützt ECMAScript, das entwickelt wurde um einen Standard für JavaScript bereitzustellen..

Um JavaScript verwenden zu können muss QF-Test mindestens mit Java 8 ausgeführt werden.

Dabei muss der ECMAScript 6 Standard in den JavaScript-Skripten verwendet werden. QF-Test führt automatisch eine interne Übersetzung auf den ECMAScript 5 Standard durch. Im Fehlerfall wird der übersetzte Code im Protokoll im Skript-Knoten aufgeführt, falls dieser vom Original-Code abweicht.

Einige Besonderheiten von JavaScript gegenüber anderen Skriptsprachen.

  • Es gibt zwei verschiedene null-Werte: undefined und null. Eine Variable ist undefined, wenn sie keinen Wert besitzt. null ist ein beabsichtigter Null-Wert der zugewiesen werden muss.
  • Der Operator == prüft auf Gleichheit statt auf Identität, so dass if (3 == "3") "true" ergibt. Um auf Identität, dss heißt Gleichheit von Typ und Wert beider Operanten, zu prüfen, gibt es den === Operator.
  • Variablen haben einen dynamischen Typ, wenn sie mit dem Schlüsselwort let deklariert werden. let x = 1 zum Beispiel erlaubt es, der Variablen x später auch einen String zuzuweisen. Konstanten werden mit const definiert.
12.5.1
Module

Auch in JavaScript können häufig benötigte Funktionen in Module ausgelagert werden. Diese müssen analog zu Jython bzw. Groovy in das javascript-Verzeichnis im QF-Test Wurzelverzeichnis gelegt werden.

Im folgenden Beispiel werden die Funktionen des Moduls moremath.js ausgelagert. Zunächst der Aufbau des Moduls:

var fibonacci = function(n) {
   return n < 1 ? 0
        : n <= 2 ? 1
        : fibonacci(n - 1) + fibonacci(n - 2);
}

function sumDigits(number) {
  var str = number.toString();
  var sum = 0;

  for (var i = 0; i < str.length; i++) {
    sum += parseInt(str.charAt(i), 10);
  }

  return sum;
}
// Module exports (Node.js style)
exports.fibonacci = fibonacci;
exports.sumDigits = sumDigits;
Beispiel 12.29:  Das Modul moremath.js

In dem Modul moremath.js sind zwei Funktionen definiert: fibonacci und sumDigits. fibonacci berechnet den Wert der Fibonacci-Zahl an der Stelle n und sumDigits bildet die Quersumme.

Jede Funktion muss exportiert werden, damit sie für den Import zur Verfügung steht. Dies geschieht mit der an Node.js angelehnten Funktion exports.

Im Skript-Knoten kann nun der folgende Code verwendet werden um auf die Funktionen des Moduls moremath.js zuzugreifen:

moremath = require('moremath');

console.log(moremath.fibonacci(13));
console.log(moremath.sumDigits(123));
Beispiel 12.30:  Verwendung des moremath.js-Moduls

Module die von QF-Test bereitgestellt werden, können über die import-Funktion importiert werden.

import {Autowin} from 'autowin';
Autowin.doClickHard(10, 10, true);
Beispiel 12.31:  Verwendung des autowin-Moduls

Java-Klassen können ebenfalls über das import Statement importiert werden.

import {File} from 'java.io';
Beispiel 12.32:  Import von Java-Klassen

Es ist auch möglich, mit der require-Funktion npm-Module zu importieren. Diese werden im nächsten Abschnitt beschrieben.

12.5.1.1
npm-Module

npm ist ein Paketmanager für JavaScript der über 350.000 Pakete zur Verfügung stellt. Unter der Webseite https://www.npmjs.com/ können die vorhandenen Pakete durchsucht werden. Es ist möglich, in einem QF-Test Skript, installierte npm-Module zu verwenden. Diese müssen im javascript-Verzeichnis des QF-Test Wurzelverzeichnisses installiert werden. Mit dem Kommando npm install underscore wird das npm-Modul underscore über die Konsole des Betriebssystems installiert. Dieses kann nun in den Skript-Knoten verwendet werden.

Es gibt npm-Module, die nicht mit Nashorn kompatibel sind. Da beispielsweise einige Funktionen verwendet werden, die nicht vom ECMAScript Standard spezifiziert werden

_ = require('underscore');
func = function(num){ return num % 2 == 0; }
let evens = _.filter([1, 2, 3, 4, 5, 6], func);
console.log(evens);
Beispiel 12.33:  Verwendung des underscore-Moduls
12.5.2
Ausgaben

Neben console.log() wurde für Ausgaben ins Terminal in QF-Test eine zusätzliche print-Methode definiert.

print([1,2,3,4]);
Beispiel 12.34:  Ausgabe eines Arrays
12.5.3
Ausführung

Die JavaScript-Skripte werden auf Server bzw. SUT-Seite nicht im Browser ausgeführt, sondern in einer eigenen Engine. Und zwar in der Oracle Nashorn Engine, welche ab Java 8 mitgeliefert wird und die Ausführung von ECMAScript in der JVM ermöglicht.