09. Januar 2025
Einführung in die Plugin-Entwicklung für QF-Test
Manch einer wird über das plugin
-Verzeichnis in QF-Test gestolpert sein – warum existiert es und wie wird es genutzt? Dieses Verzeichnis (und seine Unterverzeichnisse qftest
und sut
) dient diesen Zwecken:
Zunächst einmal werden alle .jar
-Bibliotheksdateien in diesem Verzeichnis dem QF-Test und/oder dem SUT-Classpath hinzugefügt, so dass die darin jeweils enthaltenen Klassen in Server- und SUT-Skripten, Datentreiber-Knoten (z.B. JDBC-Treiber) usw. verwendet werden können. Wenn die Bibliothek im qftest
-Verzeichnis abgelegt wird, dann sind ihre Klassen nur im QF-Test Hauptprozess und in Server-Skripten verfügbar, nicht jedoch im Classpath der getesteten Anwendung. Liegt die Bibliothek in sut
, so kann man sie nur in SUT-Skripten verwenden. Wenn sie direkt im plugin
-Verzeichnis abgelegt wird, so steht sie für beide Prozesse zur Verfügung.
Zusätzlich kann man aber auch die Testausführung durch QF-Test selbst beeinflussen oder zusätzliche Funktionalität zu QF-Test hinzufügen. Ein Beispiel dafür ist das Holistic-Plugin: Hier gibt QF-Test während der Testausführung charakteristische Töne wieder, so dass man seinem Test bei der Ausführung zuhören kann. Neben diesen scherzhaften Aspekten kann man aber die Schnittstelle, wie wir sehen werden, auch für ernsthaftere Aufgaben nutzen.
Mit QF-Test 8.0 ist die Entwicklung von QF-Test Plugins mit Unterstützung des QF-Test Gradle-Plugins 2.1.0 sehr einfach geworden – höchste Zeit also, das Thema einmal genauer anzuschauen.
In diesem Post werden wir ein sehr einfaches Plugin entwickeln, mit dem man mit einem Web-Browser die aktuelle Testausführung beobachten kann. Das Endergebnis kann auch unter gitlab.com/qfs/qftest-plugins/webstatus-pluginabgerufen werden.
Aus technischer Sicht handelt es sich bei einem QF-Test Plugin ebenfalls um eine .jar
-Datei, die im plugin
-Verzeichnis abgelegt wird. In dieser befindet sich jedoch eine spezielle Datei im Bereich META-INF/services
, mit welcher QF-Test das Plugin beim Start finden und laden kann.
Zur Entwicklung des Plugins kann jede IDE verwendet werden, die Gradle-Builds unterstützt, zum Beispiel IntelliJ. Dort erstellen wir ein neues Projekt mit dem Namen webstatus
, wählen Gradle als Build-System aus und gehen sicher, dass mindestens JDK 17 für das Projekt verwendet wird.
Ein neues IntelliJ-Projekt anlegen
Als erstes fügen wir nun dem Projekt das Gradle-Plugin hinzu, indem wir es in den plugins
-Abschnitt der Datei build.gradle
einfügen:
plugins {
id 'java'
id 'de.qfs.qftest' version '2.1.0'
}
Wenn wir nun das Projekt neu laden (z.B mit Hilfe des “Reload”-Knopfes im “Gradle”-Panel von IntelliJ) wird Gradle automatisch das aktuelle QF-Test herunterladen und den testImplementation
-Dependencies des Projektes hinzufügen.
Gradle-Panel
Um das bereits installierte QF-Test mit dem Gradle-Plugin zu verwenden muss im Abschnitt qftest
der build.gradle
-Datei der Wert version = 'local'
gesetzt werden (Falls QF-Test auf Ihrem System nicht im Standardverzeichnis installiert wurde, kann mit dem Wert versionDir
das Installationsverzeichnis explizit gesetzt werden. Nähere Informationen dazu finden sich in der Dokumentation des QF-Test Gradle-Plugins):
qftest {
version = 'local'
}
Bisher haben wir unser Projekt genauso eingerichtet, wie wir es auch für die reine Testausführung mit dem QF-Test Gradle-Plugin vornehmen würden. Um in das Projekt in den “QF-Test Plugin-Entwicklungsmodus” zu versetzen müssen wir dem qftest
-Abschnitt der build.gradle
-Datei einen weiteren Wert hinzufügen:
qftest {
version = 'local'
pluginClass = 'de.qfs.apps.qftest.plugins.WebStatusPlugin'
}
Der Wert de.qfs.apps.qftest.plugins.WebStatusPlugin
entspricht nun der Plugin-Klasse, die wir in unserem Beispiel entwickeln möchten. Wir möchten das Plugin nur im Hauptprozess von QF-Test einbetten, daher können wir das Gradle-Plugin auf diesen Entwicklungsbereich einschränken:
qftest {
version = 'local'
pluginClass = 'de.qfs.apps.qftest.plugins.WebStatusPlugin'
devScopes = ['qftest'] // default: ['qftest','sut']
}
Das Build-System ist nun eingerichtet und wir können mit der Entwicklung des eigentlichen Plugins beginnen: Dazu erstellen wir eine neue Klasse, die de.qfs.apps.qftest.shared.QFTestPlugin
implementiert:
Rahmen für die Plugin-Klasse
Die dafür notwendigen QF-Test Bibliotheken sind dabei direkt im Classpath verfügbar, da das QF-Test Gradle-Plugin die implementation
-Abhängigkeiten des Projektes entsprechend erweitert hat. Zunächst geben wir testweise eine einfache “Hello World”-Ausgabe aus:
@Overridepublicvoidinit() {
System.out.println("Hello plugin world!");
}
Um QF-Test mit dem gerade entwickelten Plugin zu starten gibt es in Gradle den startQFTest
-Task – das Gradle Plugin sorgt dafür, dass automatisch die services
-Datei passend zum gewählten Plugin-Klassennamen erzeugt und die Klasse für QF-Test auffindbar wird:
% ./gradlew startQFTest
Starting a Gradle Daemon
> Task :startQFTest
Starting QF-Test (local)
BUILD SUCCESSFUL in 7s
3 actionable tasks: 3 executed
Das Plugin wird kompiliert, QF-Test wird gestartet … Aber es erfolgt keine Ausgabe im Terminal! Wo liegt der Fehler?
Normalerweise wird der startQFTest
-Task dafür verwendet, QF-Test interaktiv und unabhängig zu starten, d.h. es werden keine Ausgaben gesammelt und der Gradle-Task wartet auch nicht, bis QF-Test wieder geschlossen wird. Um die Terminal-Ausgaben von QF-Test zu sehen, muss der Standardwert des fork
-Wertes in einer neuen Task-Definition in build.gradle
-Datei explizit geändert werden:
task startQFTestAndWait(type: StartQFTestTask) {
fork = false
}
Wenn wir nun diesen neuen Task verwenden, um QF-Test zu starten, dann sehen wir auch die erwartete Ausgabe:
% ./gradlew startQFTestAndWait
> Task :startQFTestAndWait
Starting QF-Test (local)
Hello plugin world!
<=========----> 75% EXECUTING [17s]
> :startQFTestAndWait
Die init()
-Methode wird sehr früh beim Start von QF-Test ausgeführt, gleich nachdem die Klasse geladen wurde. Um herauszufinden, in welchem Kontext die Plugin-Klasse geladen wurde, stellt die Klasse de.qfs.apps.qftest.shared.Util
zwei statische Methoden zur Verfügung: isRunningInSUT()
(Ausführung im SUT (true
) oder in QF-Test (false
)) und isRunningInteractively()
(Interaktiv (true
) oder Batch-Modus (false
)).
Möchte man bestimmte Aktionen erst ausführen, wenn QF-Test vollständig geladen wurde bzw. das SUT verbunden, so kann man auf die qftest-loaded
- bzw. die sut-loaded
- Notification warten:
import de.qfs.lib.notifications.DefaultNotificationCenter;
import de.qfs.lib.notifications.Notification;
[...]
@Overridepublicvoidinit() {
System.out.println("Plugin init");
DefaultNotificationCenter.instance().addObserver(this::qftestLoaded,"qftest-loaded");
DefaultNotificationCenter.instance().addObserver(this::sutLoaded,"sut-loaded");
}
privatevoidqftestLoaded(Notification n) {
System.out.println("QF-Test loaded");
}
privatevoidsutLoaded(Notification n) {
System.out.println("SUT loaded");
}
Bei den Notifications handelt es sich um einen mächtigen Mechanismus in QF-Test, der es erlaubt, synchrone Nachrichten (das sind wohldefinierte Zeichenketten, optional um Zusatzdaten ergänzt) im Prozess entkoppelt auszutauschen. Das Konzept ist bekannt aus dem Apple Foundation Framework.
Notifications können auch in QF-Test Skripten verwendet werden: Sobald man in einem Skript “notifications.” eingibt und dann Strg+Leertaste
drückt, erhält man eine Übersicht der vereinfachten Sende- und Empfangs-Methoden der UserNotifications
. Diese UserNotifications
können ebenfalls mit der zuvor erwähnten, erweiterten Notification-Observer-Technik verwendet werden – zum Beispiel um den Testfortschritt sehr einfach zu verfolgen. (Falls der folgende Code bei Ihnen nicht kompiliert, dann überprüfen Sie bitte, dass Sie mindestens JDK 17 für Ihr Projekt ausgewählt haben.)
import de.qfs.apps.qftest.extensions.qftest.TestRunEvent;
import de.qfs.apps.qftest.extensions.qftest.TestSuiteNode;
import de.qfs.apps.qftest.shared.script.modules.UserNotifications;
[...]
publicclassWebStatusPluginimplementsQFTestPlugin {
publicfinal Queue<TestSuiteNode> nodeStack = new LinkedList<>();
publicboolean running = false;
@Overridepublicvoidinit() {
DefaultNotificationCenter.instance().addObserver(this::runStarted,
UserNotifications.RUN_STARTED);
DefaultNotificationCenter.instance().addObserver(this::runStopped,
UserNotifications.RUN_STOPPED);
DefaultNotificationCenter.instance().addObserver(this::nodeEntered,
UserNotifications.NODE_ENTERED);
DefaultNotificationCenter.instance().addObserver(this::nodeExited,
UserNotifications.NODE_EXITED);
}
privatevoidrunStarted(Notification notification) {
nodeStack.clear();
running = true;
}
privatevoidrunStopped(Notification notification) {
running = false;
}
privatevoidnodeEntered(Notification notification) {
TestRunEvent event =
(TestRunEvent) notification.getUserInfoValue("event");
nodeStack.add(event.getNode());
}
privatevoidnodeExited(Notification notification) {
nodeStack.poll();
}
}
Um nun den Ausführungszustand im Browser anzeigen zu können benötigen wir einen eingebetteten Webserver, der die Information als Webseite bereitstellt. Unglücklicherweise ist eine Implementierung eines solchen einfachen HTTP-Servers noch nicht im Standardumfang von Java 17 (mit dem QF-Test aktuell ausgeführt wird) enthalten – dieses Modul wurde erst mit Java 18 eingeführt. Ein Blick in das lib
-Verzeichnis enthüllt jedoch, dass QF-Test zusammen mit der undertow
-Bibliothek ausgeliefert wird, die eine einfache HTTP-Server-Klasse enthält. Wenn wir nun dem devScopes
-Wert unserer gradle.build
-Datei die Zeichenkette 'undertow'
hinzufügen, so wird die entsprechende Bibliothek aus dem QF-Test lib
-Verzeichnis automatisch zu den implementation
-Dependencies des Projekts hinzugefügt:
qftest {
version = 'local'
pluginClass = 'de.qfs.apps.qftest.plugins.WebStatusPlugin'
devScopes = ['qftest', 'undertow']
}
Mit dieser Bibliothek können wir nun eine einfache WebStatusServer
-Klasse entwerfen, die bei einer Anfrage den aktuellen Ausführungszustand zurückgibt:
package de.qfs.apps.qftest.plugins;
import io.undertow.Undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.utiwl.Headers;
publicclassWebStatusServer {
publicWebStatusServer(WebStatusPlugin status) {
Undertow server = Undertow.builder()
.addHttpListener(9000,"localhost")
.setHandler(e -> {
if (status.running) {
var node = status.nodeStack.peek();
if (node != null) {
output(e, node.getType() + ": " + node.getName());
} else {
output(e,"Test execution running");
}
} else {
output(e, "Test execution stopped");
}}).build();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
final Undertow.ListenerInfo listenerInfo = server.getListenerInfo().iterator().next();
System.out.println("QF-Test status available at " +
listenerInfo.getProtcol() + ":/" + listenerInfo.getAddress());
}
protectedvoidoutput(HttpServerExchange exchange,
String response) {
exchange.getResponseHeaders().add(Headers.REFRESH,"1");
exchange.getResponseSender().send(response);
exchange.endExchange();
}
}
Diesen Webserver laden wir nun in der WebStatusPlugin
-Klasse sobald QF-Test geladen wurde:
privatevoidqftestLoaded(Notification notification) {
new WebStatusServer(this);
}
Leider kommt es nun beim Start von QF-Test zu einem Fehler:
% ./gradlew startQFTestAndWait
> Task :startQFTestAndWait
Starting QF-Test (local)
1 (...) main de.qfs.lib.notifications.DefaultNotification.getDefaultExceptionHandler().ExceptionHandler.handleException(Throwable,Notification,Observer): (#147) exception: java.lang.NoClassDefFoundError: io/undertow/Undertow (...)
Die undertow
-Bibliothek wird zwar mit QF-Test mitgeliefert, aber normalerweise nicht im Classpath von QF-Test geladen. Über Reflection können wir die Klasse aber dynamisch dem Classpath hinzufügen und initialisieren – zum Glück bietet das de.qfs.lib
-Package in QF-Test dazu einige Hilfsmethoden:
import de.qfs.apps.qftest.shared.system.Native;
import de.qfs.lib.util.DynamicClassLoader;
import de.qfs.lib.util.Reflector;
[...]
privatevoidqftestLoaded(Notification notification) {
ClassLoader cl = this.getClass().getClassLoader();
try {
DynamicClassLoader.loadJarIntoClassLoader(cl,
Native.getVersionDir() + "/lib/undertow.jar");
Class<?> serverClass =
cl.loadClass("de.qfs.apps.qftest.plugins.WebStatusServer");
Reflector.createInstance(serverClass, WebStatusPlugin.class, this);
} catch (Exception ex) {
System.err.println("Could not start WebStatus server: ");
ex.printStackTrace();
}
}
Nun können wir QF-Test starten, im Browser http://localhost:9000 öffnen und dann mit jedem neu Laden den Fortschritt bei der Testausführung beobachten!
Als letzten Schritt bauen wir nun mit ./gradlew jar
eine .jar
-Datei, die alle notwendigen Dateien enthält, und kopieren diese dann aus dem build/libs
-Verzeichnis in den Ordner plugin/qftest
von QF-Test. Damit steht nun dieses wunderbare neue QF-Test Plugin für alle Testausführungen zur Verfügung.
In einem späteren Blogpost werden wir zeigen, wie sich mit dem QF-Test Plugin-Mechanismus die QF-Test 8 Assertion API erweitern lässt. Lassen Sie bis dahin Ihrer eigenen Kreativität freien Lauf!