zurück
Avatar of Max Melzer
Autor: Max Melzer
15. Februar 2022

Parallele Webseiten-Tests mit QF-Test

Von Zeit zu Zeit kommen Kunden mit der Frage auf uns zu, ob QF-Test zur parallelen Durchführung von Tests verwendet werden kann. Die Vorteile liegen auf der Hand: Die gleichzeitige Ausführung mehrerer Tests kann die Ausführungszeit erheblich verkürzen (wenn die Systemleistung keinen Engpass darstellt) und gleichzeitig viel Infrastruktur-Overhead einsparen, da alles auf einer Maschine laufen kann.

Unsere Antwort lautet seit Jahren, dass dies "in begrenztem Maße möglich ist". Das gilt immer noch. Aber in diesem Blogbeitrag möchte ich ein wenig genauer nachschauen und erkunden, wie und in welchem Ausmaß genau dies möglich ist.

Das Schwierigste an der parallelen Ausführung von irgendetwas auf einer Maschine sind die I/O-Schnittstellen. Ein Computer hat in der Regel nur eine Maus und eine Tastatur für die Eingabe von Daten und auch nur einen Desktop um Dinge anzuzeigen.

Das bedeutet, dass Ihre Testsuiten die folgenden Kriterien erfüllen sollten, damit sie parallel laufen können:

  • Tests sollten sich nicht auf harte Systemevents zur Simulation von Tastatur- oder Mauseingaben angewiesen sein.
  • Tests dürfen sich nicht auf einen exklusiven Desktop oder eine Benutzersitzung verlassen.
  • Tests müssen in beliebiger Reihenfolge ausgeführt werden können und dürfen nicht von einander abhängig sein.

Ausführen einer Testsuite in mehreren Threads

Der erste Schritt in unserer Untersuchung ist, eine QF-Test Testsuite mehrfach parallel zu starten. Und das geht mit QF-Test sehr einfach: Wir können den Batch-Modus auf der Kommandozeile verwenden:

qftest -batch -threads 3 -run /pfad/zur/test/suite.qft

Dies wird fast gut, aber hauptsächlich schlecht funktionieren. Die Ergebnisse werden höchstwahrscheinlich ein ziemliches Durcheinander, da jeder Thread versuchen wird, denselben Client zu starten und zu kontrollieren, was letztendlich schiefgehen wird. Wir haben also noch Arbeit vor uns.

Getrennte Browser-Clients pro Thread starten

Zunächst müssen wir für jeden Thread einen separate Browser-Client starten. QF-Test unterscheidet Clients anhand deren Namen, wir könnten also die Systemvariable ${qftest:thread} verwenden, wenn wir unseren Client benennen: Im Abschnitt "Variablendefinitionen" des obersten Knotens unserer Testsuite setzen wir "client" auf etwas wie web${qftest:thread}, sodass die Clients schließlich "web1", "web2", "web3", usw. heißen. (Das Speichern des Client-Namens in einer Variable namens $(client) ist nur eine Konvention, Sie müssen also sicherstellen, dass Ihre Testsuite diese auch tatsächlich befolgt).

Dann verwenden wir etwas wie das folgende Jython-Serverskript, um ein eigenes Profil für Einstellungen, Cookies und Caches für jeden Client-Browser bereitzustellen:

 

import tempfile

client = rc.lookup("client")
profilepath = "%s/%s-profile" % (tempfile.gettempdir(), client)
rc.setLocal("profilepath", profilepath)
print "Benutze Profilpfad %s für Client %s" % (profilepath, client)

 

Wir dürfen nicht vergessen, die neue Variable $(profilepath) im Knoten "Web-Engine starten" anzugeben, damit jeder Browser ein anderes temporäres Verzeichnis für Profildaten verwendet.

Browser ohne User-Session im Headless-Modus

Wenn wir nun unsere Testsuite im Batch-Modus starten, sehen wir mehrere Browserfenster, die unsere Tests durchlaufen. Das ist schön, aber es gibt noch ein großes Problem: Die Browserfenster "klauen" sich bei jeder Interaktion gegenseitig den Fensterfokus, was die Stabilität der Tests sehr wahrscheinlich beeinträchtigen wird. Um dieses Problem zu umgehen, wechseln wir auf einen headless browser, einen Browser, der unsichtbar im Hintergrund läuft, ohne ein eigenes Fenster oder einen Desktop zu benötigen.

Um so einen Headless-Browser in unseren Tests zu verwenden, setzen wir den Browsertyp auf "headless" (oder "headless-firefox", "headless-chrome" oder "headless-edge"), in der Option "Browsertyp" des "Web-Engine starten"-Knotens. Beachten Sie, dass Headless-Browser nicht alle Features unterstützen die bei "normalen" Browsern verfügbar sind.

Alternativ können Sie mit der Einstellung rc.setOption(Options.OPT_WEB_ASSUME_HEADLESS, true) in einem SUT-Skript zwischen "Start Web Engine" und "Open Browser Window" experimentieren, um QF-Test anzuweisen, ein normales Browserfenster als headless zu behandeln.

Wenn Sie Ihre Testsuite erneut ausführen, sehen Sie ... nichts (was in diesem Fall etwas Gutes ist)! Sie sollten wahrscheinlich einige Debug-Ausgaben an strategischen Stellen Ihrer Suite hinzufügen, um sicherzustellen, dass alles wie erwartet läuft. Sie können jederzeit mit dem guten alten println (oder print() in Jython) nach stdout schreiben.

Aufteilung von Tests auf verschiedene Threads

Bis jetzt haben wir nur die immer gleichen Tests in mehreren Clients ausgeführt. Das ist noch nicht die Sorte von parallelen Tests, die uns eigentlich interessiert. Wir wollen, dass jeder Thread eine Teilmenge unserer Tests bearbeitet und nicht alle Tests auf jedem Thread ausgeführt werden.

Hierfür müssen wir unsere Teststruktur etwas anpassen. Es liegt ganz bei Ihnen, wie Sie Ihre Tests aufteilen wollen oder können. Eine einfache Möglichkeit ist die Verwendung des Modulo-Operators %, um Ihre Testfälle gleichmäßig auf die Anzahl der Threads aufzuteilen. Sie können die speziellen Variablen ${qftest:thread} und ${qftest:threads} verwenden, um herauszufinden, in welchem Thread Sie sich befinden und wie viele Threads es insgesamt gibt, und jeden Testfall mit einer "if"-Bedingung mit einer Anweisung wie dieser umgeben:

$(test_case_index) % ${qftest:threads} == ${qftest:thread}

Eine etwas kompliziertere aber auch elegantere Variante wäre es, am Anfang der Testsuite einen einfachen Dispatcher über einen TestRunListener zu registrieren. Das ginge beispielsweise mit diesem Groovy-Serverskript:

 

synchronized(binding) {
    if (! binding.hasVariable("handledSteps")) {
        binding.setVariable("handledSteps",new java.util.concurrent.ConcurrentHashMap())
        binding.setVariable("localStepIndices", ThreadLocal.withInitial({[:]}))
        // Registriere TestRunListener
        notifications.addObserver(notifications.NODE_ENTERED, { args ->
            def step = args.event.getNode()
            if (step.getType() == "TestCase") {
                def thread = Thread.currentThread()
                def handled = binding.getVariable("handledSteps")
                def id = step.getUid()
                def localStepIndices = binding.getVariable("localStepIndices")
                def localStepIndex = localStepIndices.get().get(id)
                localStepIndex = localStepIndex == null ? 0 : localStepIndex+1
                localStepIndices.get().put(id,localStepIndex)
                id = "${id}#${localStepIndex}"

                def existing = handled.putIfAbsent(id,thread)
                if (existing !== null) {
                    // Überspringe Testschritt ${id} in ${thread}, wird bereits durch ${existing} ausgeführt
                    rc.skipTestCase()
                } else {
                    // Führe Testschritt ${id} in ${thread} aus
                }
            }
        })
    }
}

 

Wir haben es geschafft! Oder?

Wenn Sie einige "print"-Anweisungen zu Ihrer Testsuite hinzugefügt haben, sollten Sie nun sehen, dass QF-Test Ihre Testbatches außer der Reihe ausführt. Glückwunsch, Sie haben es geschafft!

Trotzdem gibt es noch einige Vorbehalte.

Erstens müssen Sie beim Bearbeiten oder Hinzufügen von Tests in Ihrer Suite sehr darauf achten, dass Sie immer noch die zu Beginn dieses Beitrags festgelegten Kriterien erfüllen.

Außerdem ist der Algorithmus für die Aufteilung Ihrer Tests auf Threads aus Performance-Sicht alles andere als optimal. Verschiedene Teststapel können sehr unterschiedlich viel Zeit in Anspruch nehmen, was zu ungenutzten Threads führt. Eine umfassendere Lösung würde einen Dispatch-Scheduler beinhalten, der die Teststapel nach Bedarf freien Threads zuweist.

All diese Probleme zu beheben kann leicht sehr kompliziert werden. So kompliziert, dass es in der Tat schneller, billiger und stabiler sein kann, mehrere Instanzen von QF-Test auf separaten Maschinen oder VMs laufen zu lassen und eine bestehende Dispatch-Infrastruktur zu nutzen.

Am Ende kommen wir also wieder zurück an den Punkt, an dem wir angefangen haben: Parallele Testausführung innerhalb von QF-Test ist in begrenztem Umfang wirklich möglich. Aber Sie sollten genau prüfen, ob Ihre Testsuiten und Ihr zu testendes System geeignet sind, bevor Sie sich kopfüber in die Materie stürzen und viel Zeit und Mühe investieren.

Nachtrag 1: Lizenzierung

Wenn Sie mehrere Instanzen von QF-Test parallel laufen lassen wollen, benötigen Sie eine individuelle Lizenz für jede Instanz, genau wie wenn QF-Test in einer CI/CD Umgebung läuft.

Wenn Sie QF-Test im Batch-Modus starten, wird QF-Test Sie darauf hinweisen, wenn Ihre Lizenz für die Anzahl der Threads nicht ausreicht.

Nachtrag 2: Parallele Tests im Daemon-Modus starten

Abhängig von Ihrer Umgebung können Sie auch den Daemon-Modus von QF-Test verwenden, um Multithread-Tests ferngesteuert auszuführen.

Um den Daemon-Prozess zu starten, führen Sie den folgenden Befehl auf dem Host aus:

qftest -daemon -daemonport=3544

Anschließend können Sie mit dem folgenden Jython-Serverskript parallele Tests auf dem Host auslösen:

 

from de.qfs.apps.qftest.daemon import DaemonRunContext
from de.qfs.apps.qftest.daemon import DaemonLocator
from java.util import Properties

host = "localhost"
port = 3544
testcase = "%s" % (rc.lookup("qftest","suite.path"))
timeout = 60 * 1000

def calldaemon(host, port, testcase, timeout=0, count=1):
    daemon = DaemonLocator.instance().locateDaemon(host, port)
    trd = daemon.createTestRunDaemon()
    
    contexts = trd.createContexts(count)
    if not contexts:
        raise UserException("Could not create %d run contexts. Not enough licenses available?" % count)
        
    # start tests
    for i in range(0,count):
        contexts[i].runTest("%s#Run web tests.Test %d" % (testcase, i % 2), bindings)
        
    # wait for end
    for i in range(0,count):
        if not contexts[i].waitForRunState(DaemonRunContext.STATE_FINISHED, timeout):
            # Run did not finish, terminate it
            contexts[i].stopRun()
            contexts[i].rollbackDependencies()
            if not contexts[i].waitForRunState(DaemonRunContext.STATE_FINISHED, 5000):
                # Context is deadlocked
                rc.logError("No reply from daemon RunContext.")
            rc.logError("Daemon call did not terminate and had to be stopped.")
        result = contexts[i].getResult()
        log = contexts[i].getRunLog()
        rc.addDaemonLog(log)
        contexts[i].release()
        
    return result

result = calldaemon(host, port, testcase, timeout, 4)
rc.logMessage("Result from daemon: %d" % result)

 


Für weitere Tipps und Informationen über parallele Lasttests sehen Sie sich das Handbuchkapitel "Durchführung von Lasttests mit QF-Test" an.

Die Kommentarfunktion ist für diesen Artikel deaktiviert.

0 Kommentare