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 und Groovy ermöglicht.
3.0+
Während Jython von Anfang an dabei ist, wurde Groovy erst kürzlich in QF-Test Version 3
integriert. Es ist eine Frage des Geschmacks, welcher der beiden Sprachen man den Vorzug
gibt. Wer jedoch bereits mit Java vertraut ist, wird sich wahrscheinlich eher mit Groovy
denn mit Jython anfreunden.
In diesem Kapitel werden die Grundlagen der Skriptintegration detailliert anhand von
Jython besprochen. Das meiste davon gilt auch für Groovy, insbesondere die
Runcontext Methoden sind gleich für beide Sprachen. Auf die Besonderheiten von
Groovy wird im Abschnitt Groovy Skripting 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 beide 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.
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 die selbe 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.5, die einen Großteil der Standard Python Bibliothek unterstützt.
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.
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 Jython-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 37). 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 zusätzlich das Attribut 'Client' auf den
Namen des SUT Clients gesetzt sein, in dem es ausgeführt werden soll.
'Server Skripte' werden in einem Jython Interpreter
ausgeführt, der in QF-Test selbst integriert ist, während
'SUT Skripte' in einem im SUT integrierten Jython
Interpreter laufen. Beide 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 den Menüeintrag »Extras«-»Jython Terminal...«
können Sie ein Fenster mit einer interaktiven Kommandozeile für den in
QF-Test eingebetteten Jython Interpreter öffnen. Darin können Sie mit Jython
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 Jython Interpreter schicken. Dabei filtert QF-Test die
vom Interpreter stammenden '>>>' und '...' Markierungen heraus.
Entsprechende Jython Terminals gibt es auch für SUT Clients. Diese Terminals sind über
das »Clients« Menü zugänglich.
Hinweis Wenn Sie in einem Jython Terminal arbeiten, müssen Sie eines
beachten: Die Kommandos werden vom Interpreter nicht im Event Dispatch Thread
ausgeführt. 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.
Zur Ausführung von 'Server Skripten' und 'SUT Skripten'
stellt QF-Test eine spezielle Umgebung zur Verfügung, zu der u.a. eine
lokale Variable namens rc gehört. Diese Variable
bezeichnet das Runcontext Objekt, das den aktuellen
Zustand der Ausführung eines Tests repräsentiert. Es bietet Schnittstellen
(vollständig dokumentiert in Abschnitt 37.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 37.7.
Die beste Methode, etwas über Jython zu lernen, ist vermutlich durch
Beispiele, also werden wir im Folgenden einige einfache Beispiele
durcharbeiten. Technische Hintergrundinformationen und das Komplette
API des Runcontexts finden Sie in Kapitel 37.
Weiterführende Beispiele finden Sie außerdem in der Testsuite
doc/tutorial/demo-script.qft.
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 13.1: Meldungen aus Skripten ausgeben | |
Wird - wie empfohlen - mit kompakten Protokollen gearbeitet (vgl. die Option Kompakte Protokolle erstellen), werden normale Meldungen eventuell aus dem Protokoll entfernt
um Speicher zu sparen. Im Fall eines Fehlers werden immer die vorhergehenden etwa 100
Knoten im Protokoll aufgehoben, so dass dies im Normalfall kein Problem darstellt. Wenn
Sie eine 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)
# or simply
rc.logMessage("This message will not be removed", 1) |
|
|
| | Beispiel 13.2: Meldungen, die nicht aus kompakten Protokollen entfernt werden | |
Hinweis Nur die logMessage Methode unterstützt diesen
extra Parameter. Da Warnungen und Fehler nie aus einem Protokoll entfernt werden, macht
er für logWarning und logError keinen Sinn.
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:
|
|
var = 0
rc.check(var == 0, "!Value of var is 0")
rc.checkEqual('${system:user.language}', 'en', "English locale required",
rc.EXCEPTION) |
|
|
| | Beispiel 13.3: Checks durchführen | |
Im alten Report wird die Meldung wie ein 'Check' behandelt, wenn sie mit
einem '!' beginnt. Das optionale letzte Argument legt die
Fehlerstufe fest.
Auf Variablen von QF-Test in Jython zuzugreifen ist nicht weiter
schwierig. Es gibt jedoch zwei unterschiedliche Methoden und es ist
wichtig zu verstehen, worin sie sich unterscheiden und wann welche
Methode die bessere ist.
Zunächst einmal wird die ganz normale Variablenexpansion von QF-Test
durchgeführt, noch bevor das Skript geparst und ausgeführt
wird, d.h. Sie können Variablen der Form $(var)
oder ${Gruppe:Name} verwenden. Dies ist praktisch, wenn
Sie sicher sind, dass die Werte der Variablen Zahlen oder Boolesche
Werte sind, da Jython diese ohne Quoting versteht:
|
|
if ${qftest:batch}:
rc.logMessage("We are running in batch mode")
else:
rc.logMessage("We are running in interactive mode") |
|
|
| | Beispiel 13.4: QF-Test Variablenexpansion | |
Das obige Beispiel funktioniert prima, da
${qftest:batch} entweder zu true oder zu
false expandiert wird. QF-Test sorgt mit seiner speziellen
Umgebung dafür, dass es klappt, obwohl Jython true
und false normalerweise nicht als Boolesche Werte
interpretiert. Auch das folgende Beispiel funktioniert
einwandfrei, vorausgesetzt $(i) ist ein numerischer
Wert, z.B. ein Schleifenzähler:
|
|
# log some value
rc.logMessage("data[$(i)]:" + data[$(i)]) |
|
|
| | Beispiel 13.5: Weitere Variablenexpansion | |
Wenn Sie QF-Test Variablen mit beliebigem Textinhalt verwenden, wird es
etwas komplizierter. Diese Zeichenketten müssen für Jython mit
einfachen oder doppelten Anführungsstrichen gequotet werden:
|
|
rc.logMessage("$(someText)") |
|
|
| | Beispiel 13.6: Einfache Textexpansion | |
Der obige Code funktioniert so lange gut, bis
$(someText) zu einem Wert expandiert wird, der
Zeilenumbrüche oder Anführungsstriche enthält. In diesem Fall ist
das Skript kein gültiger Jython Code mehr und es wird eine
ScriptException geworfen.
Um dieses Problem zu vermeiden, sollten Sie sich angewöhnen, auf Textvariablen
grundsätzlich mittels der Runcontext Methode lookup (siehe Abschnitt 37.6 für API Beschreibung) zuzugreifen. Auf diese Weise müssen Sie
sich nicht mit Quotingproblemen herumschlagen.
|
|
# access a simple variable
rc.logMessage(rc.lookup("someText"))
# access a property or resource
rc.logMessage(rc.lookup("qftest", "version")) |
|
|
| | Beispiel 13.7: Zugriff auf Textvariablen mittels rc.lookup | |
Wenn Sie mehrere Variablen in einer Zeichenkette kombinieren wollen,
ist es eventuell einfacher, rc.expand an Stelle von
rc.lookup zu verwenden. Bedenken Sie dabei aber, dass
Sie die '$' Zeichen verdoppeln müssen, um zu verhindern, dass QF-Test
die Variablen selbst expandiert (vgl. Abschnitt 36.6).
|
|
rc.logMessage("The resource is" +
rc.expand("$${$$(group):$$(name)}")) |
|
|
| | Beispiel 13.8: Variablenexpansion mittels rc.expand | |
Hinweis Wir möchten noch einmal kurz den Unterschied zwischen der '$'
und der rc.lookup Methode für den Variablenzugriff wiederholen: '$'
Ausdrücke werden expandiert bevor das Skript an den Jython Interpreter übergeben
wird. Das bedeutet, dass der Text "$(var)" im Skript einfach 1:1 durch den
textuellen Wert der Variable var ersetzt wird. Im Gegensatz dazu wird bei
der rc.lookup Methode der Wert der Variablen var während der
Skriptausführung zurückgeliefert. Wie oben beschrieben, ist der zweite Weg besonders bei
Textvariablen zu bevorzugen.
Um die Ergebnisse eines Jython 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 13.9: Verwendung von rc.setGlobal | |
Nach Ausführung des obigen Skripts wird
$(fileExists) zu 1 expandieren, wenn die Datei
/tmp/somefile existiert und zu 0, wenn sie nicht existiert.
Um eine Variable zu löschen, setzen Sie deren Wert auf None. Mittels
rc.clearGlobals() aus einem 'Server Skript' können alle globalen
Variablen gelöscht werden.
Manchmal ist es hilfreich, eine Jython Variable in verschiedenen Skriptknoten 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 13.10: Globale Jython Variable | |
globalVar steht nun in allen folgenden Skriptknoten des selben 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 13.11: Löschen einer globalen Jython Variable | |
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 Jython Interpreter bereit. Für
'SUT Skripte' sind dies die Methoden toServer
und fromServer. Die entsprechenden Methoden für
'Server Skripte' heissen toSUT und
fromSUT.
Das folgende Beispiel zeigt, wie ein 'SUT Skript' direkt
eine globale Variable im 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 13.12: 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 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.
Für 'SUT Skripte' bietet der Runcontext eine äusserst
nützliche Methode. Durch den Aufruf von
rc.getComponent("componentId") werden die Informationen
aus dem 'Komponente' Knoten mit der '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 heisst, er wirft auch die
entsprechenden Exceptions, falls die Komponente nicht gefunden
werden kann.
Im Erfolgsfall wird die Komponente an das Jython Skript
zurückgegeben und zwar nicht in Form von abstrakten Daten, sondern
das konkrete Jython Objekt. Alle Methoden, die das 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 6.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 13.13: 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 13.14: Zugriff auf Unterelemente mit rc.getComponent | |
Der Runcontext kann auch dazu verwendet werden, 'Prozeduren'
in QF-Test auszuführen. Jython ist ausgezeichnet dazu geeignet, Werte
aus Dateien oder Datenbanken einzulesen, daher lassen sich mit
diesem Feature sehr einfach datengetriebene Tests erstellen.
Parameter für die 'Prozedur' werden mit Hilfe eines Jython
Dictionary Objekts übergeben. Die Schlüssel und Werte dieses
Dictionarys können beliebige Jython Objekte sein, die von QF-Test in
Zeichenketten umgewandelt werden.
|
|
rc.callProcedure("text.clearField",
{"component": "nameField"}) |
|
|
| | Beispiel 13.15: Einfacher Prozeduraufruf | |
In obigem Beispiel wird die 'Prozedur' namens "clearField" im
'Package' namens "text" aufgerufen. Der einzige Parameter
für den Aufruf hat den Namen "component" und den Wert "nameField".
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.
Viele der in Kapitel 29 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 Jython
und Groovy 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 13.16: Jython/Groovy Beispiel für 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 13.17: Jython/Groovy Beispiel für 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 29 führt für jede Option den korrekten Knoten auf.
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
Um die Funktionalität des SUT beim Erstellen von neuen
Tabellenzeilen zu testen, sollte eine QF-Test 'Prozedur' erstellt
werden, die 3 Parameter erwartet - "name", "age" und "address" - und
mit diesen eine neue Tabellenzelle anlegt und füllt. Dann können wir
Jython verwenden, um die Datei mit den Werten einzulesen und zu
parsen. Wir iterieren in einer Schleife über die Datensätze und
rufen für jede zu erstellende Tabellenzeile die 'Prozedur' auf.
Den Namen für die Datei übergeben wir in der QF-Test Variable namens
"filename". Wenn das Füllen der Tabelle abgeschlossen ist,
vergleichen wir den Endzustand der Tabelle mit den eingelesenen
Werten, 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 " + `i`)
if str(table.getValueAt(i, 1)) != data[i][1]:
rc.logError("Age mismatch in row " + `i`)
if str(table.getValueAt(i, 2)) != data[i][2]:
rc.logError("Address mismatch in row " + `i`) |
|
|
| | Beispiel 13.18: 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. Dieses Thema wird im folgenden Abschnitt
erörtert.
Es könnten Fälle auftreten in denen Sie ein 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
Komponenten-ID PriorityAwtSwingComponent alle gewohnten
QF-Test Knoten benutzen um mit dem gefundenen Textfeld zu arbeiten.
|
|
from de.qfs.apps.qftest.extensions import ResolverRegistry
panel = rc.getComponent("myPanel")
for component in panel.getComponents():
if ResolverRegistry.instance().isInstance(component, \
"javax.swing.JTextField"):
rc.overrideElement("PriorityAwtSwingComponent", component)
break
|
|
|
| | Beispiel 13.19: Beispiel 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.
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 Testsuites zur Verfügung
stellen wollen, sollten Sie im Verzeichnis jython 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-3.4.7/jython/Lib ist für Module
von Quality First Software GmbH reserviert. Jython Module haben die Endung
.py.
Um Beispiel 13.18 zu vereinfachen, könnten Sie ein
Modul namens csvtable.py schreiben, mit den Methoden
loadTable zum Lesen der Daten aus der Datei und
verifyTable zum Überprüfen der Tabelle. Ein Beispiel
dafür finden Sie in
qftest-3.4.7/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 " + `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 " +
`i` + " column " +
`j`)
ret = 0
return ret |
|
|
| | Beispiel 13.20: Schreiben eines Moduls | |
Der obige Code sollte Ihnen bekannt vorkommen. Er ist eine verbesserte
Version von Teilen von Beispiel 13.18. 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 13.21: Aufruf von Methoden in einem Modul | |
Für komplexeren Datenimport kann auch auf bestehende Python Module
zugegriffen werden. So gibt es z.B. unter http://python-dsv.sourceforge.net/
ein frei verfügbares Modul für äusserst flexiblen CSV Import.
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 (siehe auch Abschnitt 13.1). 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 den selben Effekt wie das oben beschriebene pdb.pm() haben.
Weitere Informationen zum Python Debugger entnehmen Sie bitte der Dokumentation von Python
Version 2.0.1 unter http://www.python.org/doc/2.0.1/lib/module-pdb.html.
In Jython Version 2.5 wurden große Teile der Java Version von Python grundlegend neu
implementiert. Zwar sind die meisten Änderungen rückwärtskompatibel, es gibt jedoch einige
subtile Unterschiede, sowohl in der Java Integration als auch bedingt durch Änderungen an
der Sprache Python.
Jython hat nun 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.
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.
Diese einfach 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.
In Jython 2.2.1 war der akzeptierte Workaround ein Python Idiom der Form
obj.__class__.__name__. Dies funktioniert in Jython 2.5 nicht mehr, da
statt des voll qualifizierten Namens inklusive Packages nur noch der letzte Teil mit
dem eigentlichen Klassennamen geliefert wird. Statt
java.lang.String erhält man nur noch String. Die einzige
Lösung, die für Version 2.5 zuverlässig funktioniert ist:
from java.lang import Class
Class.getName(obj.getClass())
Damit klappt es auch in Version 2.2, aber der Code ist nicht gerade intuitiv, so dass
wir ein neues Modul namens qf mit praktischen Methoden initiiert haben. Es
ist automatisch verfügbar, so dass Sie nun einfach folgendes schreiben können:
qf.getClassName(obj).
Groovy ist eine verhältnismäßig junge Sprache 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. Zur Zeit verwendet QF-Test Groovy 1.5.0.
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.codehaus.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 13.22: 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 13.23: 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 13.24: Listen und Dictionaries | |
In QF-Test Groovy-Skripten wird die $-Expansion für QF-Test Variablen nicht
unterstützt, diese gibt es nur bei Jython (siehe Abschnitt 13.3.3).
Hintergrund ist, dass Groovy das Dollarsymbol bereits zur Dereferenzierung von
Variablen und zur Auswertung von Ausdrücken in einem GString verwendet.
|
|
def x = 3
assert "$x" == 3
assert "${2 * x}" == 6 |
|
|
| | Beispiel 13.25: GString Expansion | |
Die Werte von QF-Test Variablen können zur Laufzeit des Skripts über verschiedene
rc Methoden ermittelt werden:
-
String lookup(varname) bzw. String lookup(group, varname)
-
String getStr(varname) bzw. String getStr(group, varname)
-
Integer getNum(varname) bzw. Integer getNum(group, varname)
-
Boolean getBool(varname) bzw. Boolean getBool(group, varname)
|
|
rc.setGlobal('fileExists', new File('c:/tmp/somefile.foo').exists())
assert rc.lookup('fileExists') == 'false'
assert rc.getStr('fileExists') == 'false'
assert ! rc.getBool('fileExists')
rc.setGlobal('myvar', '3')
assert rc.getNum('myvar') == 3 |
|
|
| | Beispiel 13.26: QF-Test Variablen in einem Groovy-Skript | |
Der Austausch von Variablen zwischen verschiedenen gleichartigen Skriptknoten
('Server Skripte' or 'SUT Skripte' desselben Clients) ist noch
einfacher 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.
|
|
|
|
| | Beispiel 13.27: Definieren einer globalen Groovy Variablen | |
|
|
assert myGlobal == 'global'
def globals = binding.variables
assert globals['myGlobal'] == 'global'
globals.remove('myGlobal')
assert globals.find { it == 'myGlobal' } == null |
|
|
| | Beispiel 13.28: Verwenden und entfernen einer globalen Groovy Variablen | |
Vordefinierte globale Variablen sind der QF-Test Runcontext rc und der
PrintWriter out, der von der println Methode des Skripts
verwendet wird.
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-3.4.7/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 13.29: 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 13.30: 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.