Feb 102008
 

Die vor kurzem vorgestellte Bibliothek JBoss JSFUnit soll es ermöglichen, serverseitig Unit-Tests gegen die mit Java Server Faces betriebene Web-Oberfläche auszuführen. Im Folgenden schildere ich meine Erfahrungen bei der Integration von JSFUnit in die
OpenWishes Web-Applikation.

Einführung

JSFUnit ist seit Ende November 2007 öffentlich verfügbar. Es wurde vom JBoss-Team entwickelt und soll deren Portal-Technologie automatisiert testen.

Ein JSFUnit-Test sendet Client-Anfragen an die Web-Applikation und verifiziert danach den server-seitigen Zustand der Web-Applikation. Ein JSFUnit-Test wird serverseitig mittels des Apache Cactus Tast-Frameworks ausgeführt. Die JSFUnit-API ermöglicht das Lesen und Manipulieren des JSF-Komponentenbaums mittels JSFServerSession. Ebenso können Attribute von JSF-Managed-Beans gelesen werden. Die client-seitigen Aktionen werden über ein JSFClientSession-Objekt ausgelöst. Hierdurch kann zum Beispiel ein Link aus dem JSF-Komponentenbaum mittels Aufruf “JSFClientSession.clickLink” betätigt werden, wodurch die Simulation des entsprechenden Web-Browser-Verhaltens angestossen wird.

Einbinden von JSFUnit

Bei der Integration von JSFUnit in die zu testende Web-Applikation müssen mehrere JARs in “WEB-INF/lib” aufgenommen werden. Dabei fällt auf, dass JUnit in der äteren Version 3.8.1 einzubinden ist. JSFUnit benötigt die Klasse “junit.runner.TestSuiteLoader”, die JUnit 4+ nicht mehr enthält. Mindestens Java 1.5 ist für JSFUnit erforderlich, da der Quellcode von JSFUnit Generics, etc. verwendet.

Erster Test und erstes Problem

Ein einfacher Test mit JSFUnit sieht wie folgt aus:

JSFClientSession client = new JSFClientSession("/index.jsf");
JSFServerSession server = new JSFServerSession(client);
assertEquals("/index.jsf", server.getCurrentViewID());

Aufgerufen wird der Test über einen HTTP-Request aus dem Web-Browser an das Servlet “ServletTestRunner”. Bei mir ist die aufzurufende URL:
“http://localhost:8082/openwishes/ServletTestRunner?suite=com.openwishes.presentation.jsfunit.JsfUnitTest”.
Der Test wird also in der Web-Applikation selbst ausgeführt.

JSFUnit sendet dann wiederum einen HTTP-Request an die Web-Applikation beim Konstruktoraufruf von JSFClientSession. Für die relative URL “/index.jsf” ermittels JSFUnit unter zu Hilfenahme des HTTP-Requests an ServletTestRunner die absolute URL “http://localhost:8082/openwishes/index.jsf”.

Bereits dieser einfache Unit-Test scheitert bei mir mit einer NullPointerException in “JSFServerSession.getCurrentViewID”. Hier der Code von JSFUnit an der Stelle:

public String getCurrentViewID() {
  return getFacesContext().getViewRoot().getViewId();
}

Der View-Root ist null, was zur NullPointerException führt.

Wieso ausgerechnet wieder bei mir ?

Die Demo-Web-Applikation, welche die JSF-Implementierung Apache MyFaces eingebunden hat, funktioniert problemlos mit demselben Test. Ich musste lediglich Apache Commons EL ergänzen. Ein Austausch von MyFaces durch die SUN JSF-Referenzimplementierung in Version 1.2 in Verbindung mit dem Entfernen des Session-Listeners “org.apache.myfaces.webapp.StartupServletContextListener” aus der web.xml funktionierte ebenso. Was ist also in der OpenWishes-Web-Applikation die Ursache für den Fehler?

Funktionsweise von JSFUnit (Open-Source sei Dank)

Der Konstruktoraufruf von JSFClientSession mit Parameter “/index.jsf” bewirkt einen HTTP-Request an die Web-Applikation unter Verwendung der URL “http://localhost:8082/openwishes/index.jsf”. Bei diesem Requests werden zwei Cookies übergeben: Ein Cookie mit Namen JSESSIONID, welcher die aktuelle Session-ID der Sitzung zwischen Browser-Client und ServletTestRunner beinhaltet. Das Senden des Cookies führt dazu, dass der HTTP-Request an die zu testende JSF-Seite Teil derselben Sitzung wird, in welcher die Test-Suite aufgerufen wird. Zudem wird ein Cookie mit Namen “org.jboss.jsfunit.framework.WebConversationFactory.testing_flag” gesetzt, welches den Request als Teil einer JSFUnit-Konversation markiert. Mittels JSFUnit-API “WebConversationFactory.isJSFUnitConversation” kann so geprüft werden, ob es sich um eine JSFUnit-Sitzung handelt. Da diese Cookies lediglich von JSFUnit für die HTTP-Requests intern verwendet werden, kommen diese Cookies nicht im Browser an, welcher die Ausführung der Test-Suite initiiert hat.

Über die Klasse “org.jboss.jsfunit.framework.FacesContextBridge” wird der FacesContext bereitgestellt, auf dem die JSFUnit-Tests arbeiten. Der Faces-Context ist ein Objekt von “JSFUnitFacesContext”, welcher wiederum an das tatsächliche FacesContext-Objekt der jeweiligen JSF-Implementierung (SUN JSF, Apache MyFaces) delegiert. Das JSFUnitFacesContext-Objekt wird in der Session unter dem Schlüssel “org.jboss.jsfunit.context.JSFUnitFacesContext.sessionkey” abgelegt. Das Ablegen in der Session erfolgt bereits im Konstruktor von JSFUnitFacesContext. JSFUnitFacesContext verknüpft sich zudem selbständuig mit dem aktuellen Thread durch Aufruf von “javax.faces.context.FacesContext.setCurrentInstance”.

JSFUnit verwendet Apache Cactus um die Unit-Tests serverseitig auszuführen. Den Cactus-Servlets “ServletTestRunner” und “ServletRedirector” ist der Servlet-Filter “JSFUnitFilter” vorgeschaltet. Dieser Servlet-Filter dient dem Feststellen der absoluten URL, welche die HTTP-Requests aus den JSFUnit-TestCases verwenden. Die absolute URL wird im Servlet-Context hinterlegt. Beim Verlassen des Servlet-Filters wird der JSFUnitFacesContext unter dem Schlüssel “org.jboss.jsfunit.context.JSFUnitFacesContext.sessionkey” aus der Session entfernt.

Des Rätsels Lösung

Offensichtlich erfolgen mehrere HTTP-Requests beim Aufruf von “index.jsf”, welche das Faces-Servlet erreichen. Bei der Abarbeitung des letzten HTTP-Requests wird ein neues FacesContext-Objekt erstellt, jedoch wird an diesem kein View-Root gesetzt. Das Ziel dieses letzten Request ist “http://localhost:8082/openwishes/com_sun_faces_sunjsf.js.jsf”. Es handelt sich um eine intern von SUN JSF-RI verwendete Javascript-Ressource. Methoden aus diesem Javascript-Code werden zum Beispiel beim Klicken von Links / Buttons aufgerufen, die durch die JSF-Komponenten Command-Link bzw. Command-Button erzeugt werden.

Durch den Kontextparameter “com.sun.faces.externalizeJavaScript” im Deskriptor “web.xml” wird gesteuert, ob der Javascript-Code von “com_sun_faces_sunjsf.js” als externe Ressource oder inline im HTML eingebunden wird. Bisher war die erste Variante eingestellt, da so der Browser das Javascript cachen kann. Leider zeigt eine Analyse des Quellcodes von SUN JSF-RI an den entsprechenden stellen (Klasse RenderKitUtils), dass keine weiteren Möglichkeiten für die Einbindung dieses Javascript-Codes umgesetzt sind.

Um die JSFUnit-Einbindung weiterzuverfolgen, muss somit der zusätzliche Request an das Faces-Servlet über die Verwendung des Werts “false” für den Kontextparameter “com.sun.faces.externalizeJavaScript” unterbunden werden.

Das nächste Problem ist, dass “server.getCurrentViewID()” nicht “index.jsf” liefert, sondern “org/richfaces/renderkit/html/scripts/tooltip.js”. Dies leuchtet auch ein, da diese Java-Script-Ressource über den Richfaces-Servlet-Filter ausgelifert wird, welcher dem Faces-Servlet vorgeschaltet ist.

Login-Test

Ein einfacher Test mit JSFUnit um den Login auf die OpenWishes-Web-Applikation zu testen
sieht wie folgt aus:

JSFClientSession client = new JSFClientSession("/index.jsf");
JSFServerSession server = new JSFServerSession(client);client.setParameter(
"loginForm:loginName", "neumannm");
client.setParameter("loginForm:loginPassword", "bday");
client.clickLink("loginForm:loginButtonbtn");assertEquals(Boolean.TRUE, server.getManagedBeanValue(
"#{UserAuthenticationBB.loggedIn}"));

Leider scheitert dieser Unit-Test an einer weiteren Hürde: Das Formular erfordert zwingend die Ausführung von Javascript-Code beim Klicken des Login-Button-Links.

Unterstützung für Javascript?

JSFUnit verwendet HttpUnit, um den Klick des Links nachzubilden. HttpUnit unterstützt Javascript über Rhino, ein Javascript-Interpreter geschrieben in Java.

Das Rhino JAR muss dem Class-Path der Web-Applikation hinzugefügt werden, damit HttpUnit in der Lage ist Javascript auszuführen. Doch auch nach dem Hinzufügen von Rhino funktioniert das Klicken des Links per HttpUnit API nicht. Beim Debugging zeigt sich, dass HttpUnit das JavaScript des onclick-Event-Handlers nicht ausführt. Dies liegt an einer Zeile in der Klasse “WebConversationFactory” von JSF-Unit:

HttpUnitOptions.setScriptingEnabled(false);

Diese Zeile muss auskommentiert werden, was auch schon andere Leute herausgefunden haben. Um des angepasste JSFUnit-Core-JARS zu erstellen, muss der JSFUnit-Quellcode abgerufen, entsprechend geändert und mittels Maven gebaut werden. Mit diesen Änderungen führt JSFUnit Javascript aus.

Das Ende vom Lied?

Leider stehe ich jetzt vor dem nächsten Problem: Die Javascript-Bibliothek “Prototype” welche in OpenWishes benötigt wird bereitet Rhino Probleme beim ausführen. Und so beende ich ernüchtert meine Experimente mit JSFUnit. Die Fülle der Probleme macht mir eines klar: Das Testen von Oberflächen sollte unter möglichst identischen Bedingungen automatisiert getestet werden, die auch für die manuelle Verwendung der Oberfläche gelten. Die Emulation des Browserverhaltens inklusive Javascript-Ausführung ist für meinen Geschmack eine zu starke Verfälschung des Tests. Gerade das Verhalten der Verschiedenen Browser ist doch eben für Oberflächentests von Web-Applikationen relevant. Daher befürworte ich mehr den Einsatz von Test-Werkzeugen wie Selenium, mit denen das Abspielen von aufgezeichneten Oberflächenaktionen im Browser möglich ist. Der Test von Geschäftslogik sollte meiner Meinung nach ohne die Oberfläche in abgewickelt werden.

  7 Responses to “Automatisierte Tests für Web-Applikationen – Teil 1 – JSFUnit”

  1. Versuchs doch mal mit http://selenium.openqa.org/ ein echt cooles Tool in combination mit Firefox um Browser/Regressionstests zu machen…

    Cu Mike

  2. Tatsächlich verwende ich Selenium schon eine Weile. Meiner Meinung nach erreicht man mit derartigen Testwerkzeugen eine einfach höhere Abdeckung. Da es in Verbindung mit JSF-WebApplikationen einige Punkte (feste IDs, etc.) zu beachten gibt, wird demnächst in Blogartikel zu Selenium fällig.

  3. Sehr interessanter Erfahrungsbericht! Respekt vor dem durchhalten – ich glaube ich wäre zwischendrin verzweifelt ;)

    Ansonsten kann ich deinem Fazit nur zustimmen: es sollte direkt im Zielsystem, also unter möglichst identischen Bedingungen getestet werden um ein ordentliches und aussagekräftiges Ergebnis zu erhalten.
    Serverseite Tests, die einen Container und den Context simulieren, sind meiner Meinung nach nur für Utility Klassen oder frontendunabhängigen Funktionen, welche aber zur Presentation-Tier gehören und z.b. das Request Objekt benötigen, wirklich sinnvoll. So könnte man zum Beispiel einen PhaseListener oder einen Converter auf seine Funktion in einem Unit Test allein für sich testen.

    Dazu noch ein kleiner Hinweis zu JSFUnit. Dieser beinhaltet auch die Möglichkeit sogenannte statische Tests/Analysen durchzuführen. Mit diesen statischen Tests können generelle Konfigurationselemente wie wie die View (xhtml/jsf Seiten) oder die faces-config.xml getesetet werden. Wenn zum Beispiel #{myBB.doSth} in einer JSF Seite verwendet wird, prüft JSFUnit ob auch eine myBB überhaupt konfiguriert ist.

    Diese statischen Tests von JSFUnit sind auf dem JBoss Wiki beschrieben und hören sich in ihrer Funktion ziemlich gut an. Von daher könnte JSFUnit doch noch einen guten Nutzen haben.

  4. …wird demnächst ein Blogartikel zu Selenium fällig.

    Kann man damit noch rechnen? Ein solcher Artikel würde mich sehr interessieren.
    Danke für den Erfahrungsbericht. Im Moment stehe ich vor dem Problem, wie ich meine JSF-Anwendung testen soll. Nach diesem Bericht werde ich von JSFUnit aber erstmal die Finger lassen…

  5. Hallo Marc,

    ich sitze ganz aktuell vor dem Problem einen massiven und ansteigenden Speicherverbrauch einer JSF / JPA / Facelets Applikation zu erkennen und zu beseitigen. Für den automatisierten Tests der Applikation habe ich dabei auf Canoo WebTest[1] gesetzt, welches z.B. auch bei Grails[2] zum Einsatz kommt.

    [1] http://webtest.canoo.com/
    [2] http://grails.org/

  6. In meinem akuellen Projekt hat man zu Beginn auch auf Canoo WebTest gesetzt. Die Erstellung der Tests war recht aufwendig und hat sich jetzt im Verlauf von zwei Jahren nicht gelohnt. Das Problem war die aufwendige Anpassung der Tests bei Änderungen in den Oberflächen. Ein anderes Problem ist das Problem von Testdaten, die für automatische Tests nicht wieder verwendet werden können. Die zugrundeliegende Datenbank ist einfach zu riesig und kann für automatisierte Tests nicht immer wieder in einem Ausgangszustand versetzt werden. Sämtliche Metadaten der CAD-Prozesse eines großen deutschen Autombilherstellers brauchen Platz :-)

    Nur so als Idee für Euer Problem: Kann es sein, dass Eure Sessions der User immer den hohen Speicherververbrauch erzeugen? Bei JSF ist das Problem, dass viele Leute der Einfachheit halber alle Beans im Session-Scope ablegen.

  7. Hallo Marc,

    nein, das Problem lag in einer falschen Benutzung der EntityManagerFactory. Statt sich genau eine Instanz pro DataSource zu holen, wie es die API nahe legt, wurde eine Instanz je Request geholt.

    Falsch war also

    public EntityManager getEntityManager() {
    return EntityManagerFactory.createEntityManagerFactory(PU_NAME).createEntityManager();
    }

    Richtig ist

    private static EntityManagerFactory emf = EntityManagerFactory.createEntityManagerFactory(PU_NAME);

    public EntityManager getEntityManager() {
    return emf.createEntityManager();
    }

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>