In diesem Dokument wird das Testen von SessionBeans anhand der im Dokument "Testen von Enterprise Java Beans 3.1" beschriebenen Vorgehensweisen aufgezeigt. Neben Erläuterungen der verschiedenen Möglichkeiten beim Testen der SessionBeans als Plain old Java Tests (POJT)
innerhalb eines Embedded Containers und auf dem eigentlichen Applikationsserver, soll darüber hinaus im besonderem Maße auf die Einschränkungen und die entstehenden Probleme eingegangen werden. Zur Demonstration werden unterschiedliche Beispiele für die einzelnen Vorgehensweisen
herangezogen, die Quelldateien können als RAR-Archiv heruntergeladen werden.
Für die Umsetzung wurden die folgenden Werkzeuge verwendet:
Das Testen der einzelnen Beans als POJT ist die einfachste und schnellste Möglichkeit die entwickelte Geschäftslogik auf Richtigkeit zu untersuchen. Allerdings ist der Anwendungsbereich der POJT stark beschränkt. Wie schon im Artikel über das Testen von EJBs
beschrieben, funktionieren solche Tests nur so lange, wie keine Dienste des Applikationsservers beansprucht werden.
SessionBeans nutzen häufig weitere SessionBeans oder eine bzw. mehrere verschiedene Entities für die vollständige Umsetzung der Client-Anfrage respektive der eigentlichen Aufgabenstellung. Greift innerhalb eines solchen Konstrukts beispielsweise eine weitere benötigte SessionBean auf Dienste des Applikationsserver zu,
während die zu testende SessionBean ohne zusätzliche Umgebungsdienste auskommt, bietet sich beispielsweise die Erstellung von Mock-Objekten für einen Test der Geschäftslogik an, aber auch diese Möglichkeit bringt weitere Schwierigkeiten mit sich, wie im weiteren Verlauf noch aufgezeigt wird.
Das erste Beispiel zeigt den Test für eine einfache Stateless SessionBean, welche die Funktionen eines Taschenrechners zum Addieren, Subtrahieren, Multiplizieren und Dividieren von zwei Zahlen implementiert.
Seit der EJB Version 3.0 stellen EJBs wieder Plain Old Java Objects (POJOs) und zugehörige Remote- und Local-Interfaces wieder Plain Old Java Interfaces (POJIs) dar, wodurch Beans mittels des Default-Konstruktors erzeugt werden können. Aus diesem Grund unterscheiden sich die JUnit-Tests nicht großartig von den bekannten Testfällen außerhalb des Enterprise-Bereiches. Wie JUnit Testfälle für die vorgestellte Stateless SessionBean gestaltet werden können, zeigt der nachfolgende Quellcodeauszug.
In einem weiteren Beispiel sollen zustandsbehaftete SessionBeans betrachtet werden. Dafür wurde die Calculator-Bean aus dem vorangegangenen Bespiel erweitert und die Methoden entsprechend angepasst. Die nachfolgenden Abbildungen zeigen den Quellcode der Stateful Calculator-SessionBean und des verwendeten Remote-Interfaces.
Die Stateful SessionBeans unterscheiden sich von den Stateless SessionBeans aus Sicht des Tests nicht wesentlich. Wie auch bei der Stateless SessionBean ist die Geschäftslogik in den Methoden gekapselt und kann direkt von den JUnit-Tests erreicht werden. Der größte Unterschied ist der clientspezifische Zustand, den die Stateful SessionBean besitzt. Da jedoch stets davon ausgegangen werden kann, dass der Applikationsserver funktioniert und die SessionBean in der Lage ist, den Zustand über mehrere Aufrufe hinweg zu halten, kann die Logik der Stateful SessionBean direkt von den JUnit-Tests, ohne Nutzung des Servers, getestet werden. Der JUnit-Test prüft, ob der Aufruf der Geschäftsmethoden die gewünschten Auswirkungen hat. Ob die Stateful SessionBean auch in einem EJB-Container das angestrebte Verhalten aufweist, ist mit diesem Testverfahren allerdings nicht zu klären. Um das angestrebte Verhalten sicherzustellen, sind neben Tests in einem Embedded Container oder auf dem Applikationsserver auch Integrationstests vonnöten. Der nachfolgende Quelltextauszug zeigt einen möglichen JUnit-Test für die Stateful SessionBean.
Das dritte Beispiel für das Testen mittels Plain Old Java Tests zeigt zugleich die Schwierigkeiten und die Grenzen des Testverfahrens auf. Für dieses Beispiel wurde eine Stateless SessionBean zur Zinsrechnung entwickelt, die aus einem Betrag und der Laufzeit den entsprechenden Zinssatz ermittelt und dem zukünftigen Gesamtbetrag an den Client zurückgibt. Der nachfolgende Quellcode zeigt die Implementierung.
Für die Ermittlung des Zinssatzes greift die SessionBean "ZinsrechnerImpl" auf eine weitere SessionBean "Zinssatz" zurück, die mithilfe von Diensten des Applikationsservers verschiedene Laufzeit-Zinssatz-Kombinationen generiert und diese temporär im Cache speichert. Der nachfolgende Quellcode zeigt die Implementierung der Zinsatz-SessionBean.
Aus Sicht des Entwicklers entstehen an mehreren Punkten kritische Abschnitte, die eine Implementierung eines POJT erschweren:
1. Zugriff auf die Dienste des Applikationsservers
Das erste Problem entsteht durch die Nutzung der Zinssatz-Bean durch die Zinsrechner-Bean. Die Zinssatz-Bean greift für die Berechnung des Zinssatzes gleich an mehreren Stellen auf Dienste des Applikationsservers zu. In diesem Fall wird der SessionContext dazu genutzt, einen Timer zu erstellen, der die Gültigkeit für die zinsssatzCache-Map definiert, welche die berechneten Zinssätze mit den entsprechenden Laufzeiten beinhaltet. Nach Ablauf des Timers wird die Map ungültig und die Liste wird durch die MethodeclearCache()
geleert. Die Annotation@Timeout
sorgt dafür, dass diese Methode beim Eintreten des Ereignisses automatisch aufgerufen wird. Diese und die Funktionalität des SessionContext stehen bei einem JUnit-Test mittels einfacher POJT nicht zur Verfügung und müssten bei Bedarf von Entwicklern zusätzlich implementiert werden. Da der Entwickler hier jedoch schnell einen Applikationsserver nachentwickelt hat, eignet sich diese Klasse nicht zum Testen mittels POJT. Um jedoch das Konstrukt der SessionBeans und insbesondere die Geschäftslogik der Zinsrechner-Bean zu überprüfen, kann ein Mock-Objekt der Zinssatz-Bean erstellt werden. Beispielswese kann in einem solchen Fall ein Mock-Objekt der Zinssatz-Bean erzeugt und die vorgegebene Rückgabewerte der FunktionermittleZinssatz()
vorgegeben werden. Wird dem Mock-Objekt beim Funktionsaufrufe ein Betrag von 10.000€ mit einer Laufzeit von drei Jahren übergeben, so kann das Mock-Objekt den vorher vom Entwickler gesetzten Wert 0.35 zurückgeben. Eine vollständige Umsetzung mittels Mock-Objekten zeigt das zu diesem Kapitel zugehörige Beispiel.
2. Lebenszyklus einer SessionBean
Eine weitere Herausforderung für den Entwickler ergibt sich aus der Tatsache, dass eine SessionBean nicht einfach über einen Konstruktor instanziiert wird, sondern einen Lebenszyklus durchläuft. Deshalb müssen alle Methoden, die sonst durch den Applikationsserver beim Durchlaufen des Lebenszyklus gerufen werden, nach dem Konstruieren explizit manuell aufgerufen werden. Erschwerend kommt hinzu, dass diese Callback-Methoden nicht nur innerhalb der SessionBean selbst definiert sondern auch innerhalb eines Interceptors implementiert werden können. Nach dem Instanziieren der Bean und dem Auflösen der Abhängigkeiten mittels Dependency Injection wird nach Definition des Lebenszyklus die Lifecycle-Callback-Methode aufgerufen. In diesem Beispiel wird die mit der Annotation@PostConstruct
annotierte Methodeerzeugt()
aufgerufen, die die Zinssatz-Bean mittels JNDI ermittelt und die Instanzvariable für die Zinssatz-Bean entsprechend setzt. Diese Lifecycle-Callback-Methode kann aus Sicht eines JUnit-Tests nicht aufgerufen werden, da die Namens- und Verzeichnisdienste des Applikationsservers nicht zur Verfügung stehen. Die mit@PreDestroy
annotierte Methode kann vom Entwickler ignoriert werden, da diese nur einen Ausgabebefehl ausführt. Für den Fall, dass auch hier für die Geschäftslogik wichtige Schritte stattfinden, muss der Entwickler auch diese Methode explizit aufrufen. Um die Instanziierung der Instanzvariable der Zinsrechner-Bean muss sich der Entwickler bei einem JUnit-Test ebenfalls selbst kümmern. Jedoch stellt auch das Dependency Injection eine Stolperfalle für den Entwickler dar, wie der nächste Punkt aufzeigt.
3. Dependency Injection
Die durch den Entwickler durchgeführte Dependency Injection kann zu weiteren Problemen bei der Erstellung von Tests führen. In diesem Beispiel ist die Variable zinssatz alspublic
deklariert, weshalb diese von außen einfach gesetzt werden kann. Wenn das Attribut jedoch alsprivate
deklariert wurde, ist der Ideenreichtum des Entwicklers gefragt. Beispielsweise kann, um die Attribute entsprechend zu setzen, eine Setter-Injection umgesetzt werden, der sowohl vom Container als auch von Entwicklern genutzt werden kann.
Um die Geschäftslogik der Zinsrecher-Bean zu testen, bedarf es einen größeren Aufwand, als es bei den vorherigen Beispielen der Fall gewesen ist. Ein JUnit-Test könnte beispielsweise wie folgt aussehen.
Da die Zinssatz-Bean aufgrund der engen Verzahnung mit den Diensten des Applikationsserver kein Teil des JUnit-Tests sein kann, wird von dieser SessionBean zunächst mittels Mockito ein Mock-Objekt erstellt, welches über Dependency Injection gesetzt wird. Für das Mock-Objekt werden verschiedene Kombinationen der Übergabeparameter und die entsprechenden Rückgabewerte der Methode ermittleZinssatz()
definiert,
die anschließend von der Zinsrechner-Bean für die weitere Verarbeitung genutzt werden können. An dieser Stelle muss davon ausgegangen werden, dass die Logik der Zinssatz-Bean korrekt funktioniert und sichergestellt werden, dass bei der Definition des Mock-Objekts keine Fehler unterlaufen, da diese zu falschen Ergebnissen bei der Verarbeitung innerhalb der Geschäftslogik der Zinsrechner-Bean führen können.
Wenn EJBs auf Dienste des Containers zurückgreifen, ist ein Testen in einer möglichst realen Umgebung unumgänglich. Statt diese Dienste nachzuimplementieren und damit wertvolle Zeit zu verlieren, die sonst für ein ausgiebiges Testen verwendet werden sollte, können sogenannte Embedded Container verwendet werden. Die Vorteile solcher Lösungen liegen klar auf der Hand. Zum einen können Dienste der Umgebung genutzt werden und damit auch die vom Entwickler implementierte Logik für das Zusammenspiel mit dem Server in einer realen Umgebung getestet werden, zum anderen arbeitet ein solcher Embedded Container wesentlich schneller als die Applikationsserver wie beispielsweise JBoss, sodass ein Testdurchlauf viel schneller und ohne explizites Deployment und Undeployment durchgeführt werden kann. Kandidaten für solche Tests sind beispielsweise CUBA oder OpenEJB. Für dieses Kapitel wird OpenEJB in der Version 4.5.1 verwendet. Im vorherigen Kapitel wurden die Grenzen des Testens mittels POJT aufgezeigt, die besonders beim dritten Beispiels "Zinsrechner" zum tragen kamen. Dieses Kapitel zeigt einen Weg wie dieses Beispiel vollständig getestet werden kann. Auch unter Verwendung des Embedded Containers OpenEJB können herkömmliche JUnit-Tests herangezogen werden wie der nachfolgende Quellcodeausschnitt aufzeigt.
Innerhalb der setUp
-Methode müssen die Properties für den InitialContext
erstellt und gesetzt werden. Über den Aufruf der ersten setProperty()
-Methode wird ein InitialContext org.apache.openejb.client.LocalInitialContextFactory
erstellt, welche dafür sorgt, dass beim Testdurchlauf der OpenEJB-Container gestartet wird. Nach dem Start des Containers durchsucht selbiger alle Klassenpfad-Einträge nach EJBs, die entweder
einen Deployment-Deskriptor enthalten oder die mittels des Parameters openejb.deployments.classpath.include
dem InitialContext
übergeben wurden und macht die EJBs entsprechend verfügbar. Mittels des Parameters openejb.deployments.classpath.exclude
können explizit Klassenpfade ausgeschlossen werden.
Der erste Testfall testZinsrechnerBean
zeigt beispielhaft wie die EJBs mit OpenEJB ermittelt werden können. Der JNDI-Eintrag setzt sich aus {deploymentId}
und {interfaceType.annotationName}
zusammen, demnach den Namen der Bean und entweder Local für das lokale oder Remote für das entfernte Interface. Dies ist die Default-Variante von OpenEJB, die jedoch nach Bedarf modifiziert werden kann, wie die Webseite aufzeigt.
Schwierigkeiten können vor allem dann entstehen, wenn SessionBeans lookups von anderen Beans durchführen und der Name respektive der Pfad nicht zum Schema von OpenEJB passen. Die nachfolgende Abbildung zeigt einen solchen Aufruf in der @PostConstruct
-Methode der Zinsrechner-Bean.
Der Pfad "ejb:/EJB_HS-Osnabrueck//ZinssatzImpl!de.hs.osnabrueck.server.sessionbean.stateless.Zinssatz
" stellt in JBoss den Pfad zum Auffinden von EJBs dar. Allerdings kann dieser in der Form nicht von OpenEJB verwendet werden und führt in JUnit-Testdurchläufen unweigerlich zu Fehlern bei allen Tests, die die zu ermittelnde EJB verwenden. Für einen - eventuell sogar automatisierten - Test alle Pfade abzuändern, stellt keine befriedigende Lösung dar. Vielmehr bietet es sich an, ein Format zu verwenden,
was sowohl von JBoss als auch von OpenEJB interpretiert werden kann. Im Folgenden werden zwei verschiedene Ansätze vorgestellt.
Um das Dependency-Lookup zu verwenden, muss vor der Klassendeklaration die Annotation @EJB
gesetzt werden. Dabei können, wie die nachfolgende Abbildung zeigt, sowohl der Name, der Bean-Name und das Bean-Interface entsprechend gesetzt werden.
Der optionale Name gibt den Pfad an, unter dem die referenzierte Bean im lokalen Namensraum (java:comp/env/
) der Komponente auffindbar sein soll. Der Bean-Name stellt den logischen Namen der referenzierten Bean dar, welcher entweder durch den Klassennamen oder den Namen in den Annotationen @Stateless
, @Stateful
und @MessageDriven
definiert wird. Durch das Bean-Interface wird der Interface-Typ der referenzierten Bean angegeben. Dies können sowohl ein Local- als auch ein Remote-Interface sein.
Der Nachteil des Dependency-Lookups liegt in der Verschmutzung des fachlichen Quellcodes mit technischen Details. Der Entwickler muss sich um die Referenz kümmern und Exceptions des Namendienstes abfangen und entsprechend behandeln.
Bei der Verwendung von Dependency-Injection wird die Verantwortung für das Auffinden der Referenzen an den Applikationsserver weitergegeben, was auch als Inversion-Of-Control bezeichnet wird, da die Klasse, welche die Referenz benötigt, diese nur noch nutzt und es dem Aufrufer der Klasse überlässt, die Referenz zu beschaffen und zu setzen. Daraus resultiert auch der erste große Unterschied bei der Implementierung im Gegensatz zum Dependency-Lookup. Innerhalb der @PostConstruct
-Methode wird kein
expliziter Lookup mehr benötigt. Die @EJB
-Annotation findet ebenfalls Verwendung, wird jedoch bei Dependency-Injection innerhalb der Klassendefinition und oberhalb der Instanz- respektive der Klassenvariable gesetzt. Die nachfolgende Abbildung zeigt dies beispielhaft.
Die Bedeutung der Parameter ist identisch mit denen bei der Verwendung des Dependency-Lookups, der "name"-Parameter entfällt in diesem Falle jedoch. Sowohl für Dependency-Injection als auch für Dependency-Lookup können statt der Annotationen entsprechende Einträge innerhalb des Deployment-Deskriptors verwendet werden.
Für das Testen auf dem Applikationsserver müssen lediglich die Client-Aufrufe respektive die JUnit-Testfälle angepasst werden. Im Folgenden wird ein Testfall beispielhaft für den Zinsrechner, welcher auf dem JBoss AS 7.1. Applikationsserver bereitgestellt wird, vorgestellt und entsprechend erläutert. Die Nachteile dieses Vorgehens sind durch den Quellcode nicht vollständig ersichtlich, weshalb diese nochmals kurz zusammengefasst werden, bevor auf das eigentliche Beispiel eingegangen wird. Der Applikationsserver wird an zentraler Stelle betrieben und wird nicht mittels oder durch einen JUnit-Testfall gestartet. Auch das Deployment findet nicht im Rahmen des JUnit-Testfalls statt, wodurch ein Deployment im Vorfeld stattfinden muss. Auch muss der Applikationsserver vor dem Test hochgefahren werden und betriebsbereit sein, was ein "schnelles" Testen unmöglich macht. Ist ein Fehler durch einen Testfall aufgedeckt worden, müssen die Quelldateien bearbeitet und die aktualisierte Version auf dem Server deployed werden, bevor ein erneuter Testdurchlauf gestartet werden kann. Ehe auf dem Applikationsserver getestet wird, empfiehlt es sich daher, zuerst einfache POJT und anschließend Tests innerhalb eines Embedded Containers durchzuführen, um möglichst viele Fehler bereits im Vorfeld ausschließen zu können, wodurch sich ein enormes Einsparungspotential der sonst sehr begrenzt vorhandenen Ressource Zeit ergeben kann. Die nachfolgenden Quellcodeauszüge zeigen den Testfall für die Zinsrechner-Bean auf einem JBoss AS 7.1 Applikationsserver.
Innerhalb der Klasse ZinsrechnungClientTestCase
wird der JUnit-Testfall ausgeführt. Dazu wird zunächst innerhalb der setUp()
-Methode ein Lookup der Bean durchgeführt. Inmitten der doLookup()
-Methode wird zunächst der InitialContext
erstellt. Dazu wird auf die Klasse ClientUtility
zurückgegriffen, die im zweiten Quellcodeausschnitt dargestellt wird und deren Methoden bereits aus den vorherigen Abschnitten bekannt sein dürften. Um den richtigen String zu Ermittlung der gesuchten Bean auf dem JBoss AS 7.1
Server zu erhalten, wird die Methode getLookupName
aufgerufen. Der allgemeine Pfad zum Aufruf einer Bean auf einem JBoss AS 7.1 Server ist definiert durch "ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>
" für eine Stateless SessionBean und "ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>?stateful
" für eine Stateful SeassionBeam, welcher in der getLookupName
-Methode entsprechend zusammengesetzt
wird. In der mit @Test
annotierten Methode testeSparsumme wird anschließend der Testfall ausgeführt.
[01] W. Everling, J. Leßner: Enterprise JavaBeans 3.1: Das EjB3-Praxisbuch für Ein- und Umsteiger, 2. Auflage, Carl Hanser Verlag, München, 2011
[02] O. Ihns, S. Heldt, et. al: EJB 3.1 professionell - Grundlagen und Expertenwissen zu Enterprise JavaBeans 3.1 - inkl. JPA 2.0, 2., aktualisierte und erweiterte Auflage, dpunkt.verlag, Heidelberg, 2011