Startseite

Test Suite - Performance Test Automat

Meine Test Suite dient dazu, die Performance und Funktion von HTTP basierten Diensten zu überprüfen.

Eine wichtiger Teil bei der Entwicklung von Web Anwendungen ist das automatisierte Testen - sei es um die Performance zu Überprüfen oder um nach Programmänderungen schnell herauszufinden, ob man versehentlich andere Teile kaputt gemacht hat.

Dazu gibt es natürlich bereits zahlreiche kostenlose Lösungen. In den kommerziellen Projekten, wo ich mitgewirkt hatte, waren diese Testautomaten jedoch stets unzureichend. Meistens waren sie schlicht zu langsam, um den Server ausreichend zu belasten. Häufig scheiterten sie auch daran, bestimmte Schnittstellen jenseits von HTTP zu unterstützen. Ein ganz besonders schwieriger Punkt war die Unterstützung von Single-Page Anwendungen mit Rich Faces. Eine vielversprechende kommerzielle Lösung, deren Namen ich vergessen habe, kam nicht in Frage weil der Anbieter für die damals gewünschten 400 gleichzeitigen Threads unglaublich hohe Lizenzkosten verlangte.

Deswegen hatte ich vor ca. 10 Jahren diese Test Suite entwickelt. Sie hat sich bis heute in zahlreichen Projekten bewährt, so dass ich mich entschlossen habe, sie trotz der spärlichen Dokumentation zu veröffentlichen.

Anforderungen

Bei der Entwicklung spielten folgende Anforderungen eine wichtige Rolle:

Mit diesem Funktionsumfang ist die Test Suite zwar keine Eierlegende Wollmilchsau, aber ein guter Anfang für die Programmierung eines anwendungs-spezifischen Testautomaten. Ich veröffentliche dieses Projekt ganz bewusst nur als Quelltext, weil der Programmierer sehr wahrscheinlich anwendungs-spezifische Sonderlocken hinzufügen wird. Dieser Ansatz ist besser, als handelsübliche Frameworks, die alleine schon aufgrund ihrer gewaltigen Größe häufig zu träge arbeiten. Denn letztendlich will man ja die Leistung des Servers testen, nicht die Leistung des Testautomaten.

Wie man das Projekt benutzt

Das Projekt ist so aufgebaut, daß man es wahlweise mit Eclipse oder Netbeans bearbeiten kann. Man kann die Tests sowohl innerhalb der IDE als auch auf der Kommandozeile ausführen.

Konfigurationsdatei

Als Erstes sollte man sich die Datei configuration.properties anschauen. Hier sind einige Einstellungen bereits vorgegeben, weitere kann man bei Bedarf hinzufügen.

SERVER_URL = http://192.168.0.123
Definiert den Anfang der URL's, die zu dem Server führen, der getestet werden soll. Der HTTP Client schreibt dies vor alle URL's, wenn sie nicht bereits mit "http" beginnen.

SESSIONCOOKIE = PHPSESSID
Dies ist der Name des Session Cookies. Wenn der zu testende Server keine Cookies verwendet, spielt diese Einstellung keine Rolle. Das Cookie wird automatisch gespeichert und in jedem folgenden Request als HTTP header mit gesendet.

CONNECT_TIMEOUT = 5000
Legt fest, nach wie vielen Millisekunden der Server die HTTP Verbindung annehmen muss. Wenn er zu langsam ist, bricht der Test mit einer entsprechenden Fehlermeldung ab.

READ_TIMEOUT = 10000
Legt fest, nach wie vielen Millisekunden der Server normale Anfragen beantworten muss. Wenn er zu langsam ist, bricht der Test mit einer entsprechenden Fehlermeldung ab.

SLOW_READ_TIMEOUT = 60000
Wie READ_TIMEOUT, jedoch für besondere Fälle wo eine langsamere Antwortzeit ausdrücklich erlaubt sein soll.

ENCODING = UTF-8
Zeichensatz in dem die Antworten des Servers erwartet werden.

LANGUAGE = de
Die Sprache wird als HTTP Request Header an den Server gesendet. Damit kann man bei vielen Web-Anwendungen die Anzeigesprache beeinflussen.

REPORT_FORMAT = text
Dieser Parameter bestimmt, in welchem Format der Bericht erstellt werden soll. Man kann auch csv oder html erzeugen.

Falls man HTTPS in Kombination mit selbst signierten Zertifikaten verwendet, benötigt man zusätzlich die beiden folgenden Einstellungen:

javax.net.ssl.trustStore = certificate/keystore.jks
Gibt eine Datei an, in der zusätzliche Zertifikate abgelegt sind, denen man vertraut. Im Unterverzeichnis certificate findest Du eine Beispiel-Datei sowie eine README Datei, die erklärt, wie man weitere Zertifikate importiert.

javax.net.ssl.trustStorePassword = test1234
Mit diesem Passwort wird der Zertifikats-Speicher geöffnet. Derjenige, der die Datei erzeugt hat, kennt das Passwort.

Das Programm kann viele Threads gleichzeitig oder zeitlich versetzt starten. Dazu dienen die folgenden Einstellungen:

THREADS = 10
Anzahl der Threads, die man ausführen möchte. Jeder Thread führt eine eigene Instanz der Test Suite jeweils genau einmal aus. Wobei man in die Test-Suiten Wiederholschleifen einbauen kann, was für Dauer-Last-Tests hilfreich ist. Aber normalerweise führen die Test-Suiten ihre programmierten Aktionen nur einmal aus und enden dann.

THREAD_STARTUP_DELAY = 100
Wenn der Wert 0 ist, werden alle Threads beinahe gleichzeitig gestartet (eine super Methode, um Server lahm zu legen). Ansonsten wird nach dem Starten jedes Threads so lange gewartet, bevor der nächste Thread startet.

Darüber hinaus gibt es noch die Konfigurtionsdatei logging.properties, wo man das Format der Protokolldatei beeinflussen kann und einstellen kann, wie detailliert die Meldungen der einzelnen Java Packages sein sollen. Die interessantesten Einträge darin sind diese:


.level = SEVERE
de.stefanfrings.test.framework.level = WARNING
my.package.level = FINEST
Diese Einstellungen bedeuten, dass die Klassen aus dem Package "de.stefanfrings.test.framework" nur Warnungen und Fehler protokollieren sollen. Aber die Klassen aus dem package "my.package" sollen alle möglichen Details ausgeben. Für alle ungenannten Packages gilt die erste Zeile, welche festlegt, dass nur Fehler (SEVERE) protokolliert werden. Mehr Doku zur Konfiguration von java.util.logging gibt es hier.

Ausführen

Nach der Entwicklung möchte man den Testautomat warscheinlich auf einer anderen Maschine ausführen, der über eine schnelle Netzwerverbindung zum Server verfügt. Dazu benötigt man das compilierte Programm als jar Datei, welche NetBeans nach dem Build im Verzeichnis dist ablegt. In Eclipse erzeugt man diese Datei, indem man das Projekt exportiert (File/Export.../Java/JAR-File). Diese jar Datei muss man auf den Rechner kopieren, wo sie ausgeführt werden soll. Und natürlich muss dort eine Java Runtime (oder Java Development Kit) installiert sein.

Um eine TestSuite auf der Kommandozeile auszuführen gibt man ein:

java -cp TestSuite.jar de.stefanfrings.test.suite.PerformanceTest

Die Konfigurationsdatei (configuration.properties) ist in diesem JAR File enthalten. Falls man davon abweichend einen anderen Server testen möchte, kann man die SERVER_URL so überschreiben:

java -DSERVER_URL="http://woanders.de" -cp TestSuite.jar de.stefanfrings.test.suite.PerformanceTest

Oder alternativ kann man das JAR File auspacken (es ist in Wirklichkeit eine ZIP Datei), die Konfigurationsdatei editieren und dann das Programm so starten:

java de.stefanfrings.test.suite.PerformanceTest

Berichte

Das Programm gibt die Berichte immer auf die Konsole aus. Man kann sie bei Bedarf einfach in eine Datei umleiten. Ein Bericht im Text-Format sieht zum Beispiel so aus:
-----------------------------------------------------------------
Report of MeineTestSuite with 10 threads
-----------------------------------------------------------------
Bestellung
  login                        101-435ms  (~321ms)     10 tests Ok
  sucheArtikel                 720-934ms  (~866ms)     10 tests Ok
  bestelleArtikel              73-211 ms  (~199ms)     2/10 failed      (IOException Server returned HTTP response code: 500 for URL: http://192.168.0.123)
Total time: 1847 ms, 1949 executions per hour
Die Ausgabe bedeutet folgendes:

Wer die "PerformanceTest" Suite unverändert ausführt, wird folgenden Bericht erhalten:

-----------------------------------------------------------------
Report of PerformanceTest with 10 threads
-----------------------------------------------------------------
Login
  open                        5001-5049ms (~5006ms)    10/10 failed   (SocketTimeoutException connect timed out)
  login                       5001-5002ms (~5001ms)    10/10 failed   (SocketTimeoutException connect timed out)
Total time: 10910 ms, 3299 executions per hour
Das bedeutet, dass alle Testfälle fehlgeschlagen sind, weil der Verbindungsaufbau zum Server fehlschlug. Das ist logisch, denn der konfigurierte Server "http://192.168.0.123" existiert gar nicht.

Programmierung

Zunächst möchte ich darauf hinweisen, dass dieses Projekt lediglich einen HTTP Client und einen Scheduler für die Threads enthält. In der Grundausstattung kommt es ganz ohne Java Libraries aus, man benötigt lediglich das Java Development Kit (ab Version 7). Für andere Verbindungen (zum Beispiel Datenbank) muss man allerdings die dazu nötigen Libraries hinzufügen.

Test Klasse

Die Programmierung eines Testautomaten beginnt damit, dass man mindestens eine Test-Klasse schreibt. Für einen schnellen Start habe ich die beiden Klassen de.stefanfrings.test.cases.Login und de.stefanfrings.test.cases.Logout bereit gestellt. Verwenden Sie diese als Kopiervorlage. Sie sollten das Package allerdings entsprechend der Namenskonvention ihrer Firma umbenennen.

Jede Test-Klasse hat eine run() Methode, welche die einzelnen Testfälle in der richtigen Reihenfolge ausführt, sowie eine main() Methode die es uns ermöglicht, die Klasse mal eben schnell in der IDE auszuführen.

public class Login extends TestClass
{

    public void run() 
    {
        call("open");
        call("login");
    }
    
    public static void main(String args[]) 
    {
        new Login().run();
        report.print("Report of " + Login.class.getSimpleName(), 1, 1);
        System.exit(report.isAllSuccessful() ? 0 : 1);
    }
}
In NetBeans klickt man zum Ausführen mit der rechten Maustaste in den Quelltext oder auf den Dateinamen und dann auf "Run File". In Eclipse klickt man mit der rechten Maustaste auf den Dateinamen und dann auf "Run As java Application". Wenn man die Testklasse so ausführt, läuft nur ein einziger Thread. Für Mult-Threaded Ausführung muss man stattdessen eine Test-Suite ausführen (siehe weiter unten).

Jeder Testfall ist eine Methode in der Test Klasse. Der Login Vorgang besteht aus zwei Testfällen, nämlich open() und login():

    public String open() throws Exception
    {
        loadPage("/login.html");
        mustContain("Geben Sie name und passwort ein");
        return null;
    }

    public String login() throws Exception 
    {
        Map<String, String> postParams = new HashMap<>();
        postParams.put("name", "dagobert");
        postParams.put("password", "duck");
        
        submitForm("/login.php", postParams);        
        expectJsonValue("0", "code");
        return null;
    }
Der "open" Testfall öffnet die Login-Webseite und prüft, ob diese Seite den Text "Geben Sie name und passwort ein" enthält. Wenn das nicht der Fall ist, dann hat der Server nicht die erwartete Seite geliefert, so dass es als Fehler gewertet wird. Der Rückgabewert return null signalisiert, dass der Test erfolgreich war. Im Fehlerfall soll man stattdessen eine Fehlermeldung zurückliefern oder eine Exception werfen, wie es die mustContain() Methode tut.

Die "login" Methode simuliert das Senden eines ausgefüllten HTML Formulars. Die Variable postParams enthält dabei die Feldnamen und Werte des Formulars. Mit expectJsonValue() wird kontrolliert, ob die Antwort des Servers (die im JSON Format erwartet wird) in Ordnung ist. Zum Beispiel:

{
    "response": {
        "code": 0,
        "message": "success"        
    }
}

Test Suiten

Wenn man den Testautomaten später benutzt, dann führt man immer eine der Test-Suiten aus. Diese dienen dazu, mehrere Testklassen zu einem vollständigen Testlauf zusammen zu fassen und nacheinander in mehreren Threads auszuführen.

Das Projekt stellt als Kopiervorlage die Klasse de.stefanfrings.test.suite.PerformanceTest zur Verfügung. Auch hier sollten Sie wieder das Package entsprechend der Namenskonvention ihrer Firma umbenennen.

Jede TestSuite enthält nur zwei Methoden, und zwar run() und main(). Auch diese kann man direkt in der IDE ausführen:

public class PerformanceTest extends TestSuite
{

    public void run()
    {
        new Login().run();
        new Logout().run();
    }

    public static void main(String args[])
    {
        new PerformanceTest().executeMultiThreaded(1);
        System.exit(report.isAllSuccessful() ? 0 : 1);
    }
}
Um einen Dauer-Last-Test zu bauen, fügt man eine Wiederhol-Schleife mit Zähler ein. Der Zähler wird benötigt, um die Reports der einzenen Durchläufe zu gruppieren.
    public static void main(String args[])
    {
        int counter=1;
        while (true) // unendlich oft wiederholen          
        {
            new PerformanceTest().executeMultiThreaded(counter++);
        }
        System.exit(report.isAllSuccessful() ? 0 : 1);
    }

Was das Framework bereit stellt

Das Framework dieses Projektes stellt folgende Sachen bereit, die man in Test-Klassen verwenden mag:

Das Properties Objekt config repräsentiert die ganze Konfigurationsdatei.
Zusätzlich gibt es zur Bequemlichkeit die folgenden Variablen:

Variablen:

Methoden:

Map<String, Object> sessionData()
Liefert einen Container zum Speichern von session Daten. Jeder Thread hat seinen eigenen Container. Er ist dazu gedacht, dass ein Testfall irgendwelche Daten an die nachfolgenden Testfälle übergeben kann, zum Beispiel die sessionID nach einem erfolgreichen Login.

boolean isLoggedIn()
Liefert true zurück, wenn die Session Daten eine "sessionID" enthalten.

String doHTTP(String urlPart, int readTimeout, Map httpRequestHeader, Map httpPostData, String documentBody)

Sendet einen HTTP Request an die angegebene URL. Wenn die URL nicht mit "http" beginnt, dann wird die konfigurierte SERVER_URL davor gepackt. readTimeout bestimmt, wie lange maximal auf die Antwort gewartet werden soll, hier gibt man normalerweise entweder READ_TIMEOUT oder SLOW_READ_TIMEOUT an. httpRequestHeader kann optional HTTP Header festlegen, die gesendet werden sollen. Außerdem kann man entweder httpPostData oder documentBody verwenden, um Daten an den Server zu senden. Der Unterschied ist, dass httpPostData die name/werte Pärchen so sendet, wie Web-Browser Formulare senden, während documentBody einen String 1:1 sendet, was man überlicherweise mit XML oder JSON benutzt.

Ich benutze doHTTP normalerweise nicht direkt, sondern rufe stattdessen die folgenden Bequemlichkeits-Helfer auf:

Die obigen Funktionen benutzen alle READ_TIMEOUT. Weiterhin gibt es auch "Slow" Varianten (z.B. loadSlowPage()) die stattdessen den SLOW_READ_TIMEOUT benutzen. Diese sind für besondere Fälle gedacht, die ausnahmsweise länger dauern dürfen.

Mit den folgenden Funktionen kann anschließend die Antwort (also der Inhalt von der Variable document) überprüft werden:

boolean contains(String... keywords)
Findet heraus, ob empfangene Dokument die angegebenen Stichwörter in genau dieser Reihenfolge enthält.

void mustContain(String... keywords)
Bricht mit einer Exception ab, wenn das empfangene Dokument nicht alle angegebenen Stichwörter in genau dieser Reihenfolge enthält.

String getFormField(String... keywords)
Findet in dem zuvor empfangenen HTML Dokument ein Formular und extrahiert den Wert eines Formular-Feldes. Als Parameter gibt man beliebig viele Stichwörter an, die in genau dieser Reihenfolge in dem HTML Dokument vorkommen müssen. Das letzte Stichwort entspricht dem Namen des Formular-Feldes. Zum Beispiel: <input type="text" name="feldName" value="wert">

String getElementValue(String... keywords)
Findet ein HTML oder XML Element und liefert dessen Wert zurück. Als Parameter gibt man beliebig viele Stichwörter an, die in genau dieser Reihenfolge in dem HTML/XML Dokument vorkommen müssen. Das letzte Stichwort ist der Name des gesuchten Elementes. Zum Beispiel: <name>wert</name>

String getAttribute(String... keywords)
Findet ein HTML oder XML Element und liefert davon ein bestimmtes Attribut zurück. Als Parameter gibt man beliebig viele Stichwörter an, die in genau dieser Reihenfolge in dem HTML/XML Dokument vorkommen müssen. Das vorletzte Stichwort ist der Name des gesuchten Elementes, und das letzte Stichwort ist der Name des gewünschten Attributes. Zum Beispiel: <name attributName="wert"></name>

String getJsonValue(String... keywords)
Findet in einem JSON Dokument einen Wert anhand seines Namens. Als Parameter gibt man beliebig viele Stichwörter an, die in genau dieser Reihenfolge in dem JSON Dokument vorkommen müssen. Das letzte Stichwort ist der Name des gesuchten Wertes. Zum Beispiel: "name": "wert"

void expectJsonValue(String expected, String... keywords)
Findet in einem JSON Dokument einen Wert anhand seines Namens und prüft ob dessen Wert dem erwarteten Wert entspricht. Als Parameter gibt man beliebig viele Stichwörter an, die in genau dieser Reihenfolge in dem JSON Dokument vorkommen müssen. Das letzte Stichwörter ist der Name des gesuchten Wertes.

String getUrlParameter(String... keywords)
Findet in einem HTML Dokument eine URL und liefert den Wert eines bestimmten URL-Parameters zurück. Als Parameter gibt man beliebig viele Stichwörter an, die in genau dieser Reihenfolge in dem HTML Dokument vorkommen müssen. Das letzte Stichwort benennt den URL Parameter. Beispiel: http://irgendwo.de/blabla?parametername=wert

String getUpdateValue(String... keywords)
Diese Funktion ist speziell für Frontends mit RichFaces gedacht. Wenn der Server nach einem AJAX Request in der Webseite ein Formularfeld aktualisiert, dann findet diese Methode den neuen Wert heraus. Als Parameter gibt man beliebig viele Stichwörter an, die in genau dieser Reihenfolge in der AJAX Response vorkommen müssen. Das letzte Stichwort benennt das Formularfeld, das aktualisiert wurde. Beispiel: <update id="name"><![CDATA[wert]]>

Mit den folgenden Funktionen gann man ganz grob prüfen, ob der Server eine Datei im erwarteten Format geliefert hat:

Multi-Threading und Variablen

Beim Programmieren muss man stets daran denken, dass der Testautomat Multi-Threaded arbeitet. Jeder Thread führt eine eigene Instanz der Testklasse aus und hat somit normalerweise auch seine eigenen Variablen. Manchmal möchte man jedoch, dass die ganzen Threads sich eine Variable teilen. Diese muss dann als static deklariert werden und die Zugriffe darauf müssen synchronisiert werden, damit es nicht zu Konflikten wegen gleichzeitigen Zugriffen kommt.

Ein typischer Anwendungsfall wäre die Erzeugung einer fortlaufenden Nummer. Angenommern, im ersten Thread soll der Artikel 101 gekauft werden, der zweite Thread soll Artikel 102 kaufen, der dritte Thread soll Artikel 103 kaufen, und so weiter. Dann braucht man eine gemeinsam genutzte Integer Variable, die bei jeder Verwendung um eins erhöht wird. Der entsprechende Java Code könnte so aussehen:

public class KaufeEinenArtikel extends TestClass 
{
   static int number=100;
   
   public static synchronized int nextNumber()
   {
       return ++number;
   }
   
   
   public String zeigeArtikel()
   {    
       loadPage("/katalog/artikelnummer="+nextNumber());
       return null;
   }
}