26.09.2012
Unit-Tests für EJBs mit einem Embeddable-Container
Seit JEE6 (Java Enterprise Edition 6) und EJB 3.1 (Enterprise JavaBeans) ist es möglich EJBs mittels eines Embeddable-Container zu testen. D.h. ein EJB kann getestet werden, ohne es aufwendig auf einem Application Server zu deployen. Es wird einfach ein lokaler EJB-Container innerhalb der Java Virtual Machine (JVM) zu Beginn des Tests gestartet und anschließend wieder herunter gefahren.
Das folgende Beispiel setzt auf dem JEE6-Tutorial auf und testet die Klasse UrlManagerBean
. Oder genauer gesagt, die Methode public List<String> getUrls()
.
Hier geht es nicht darum zu zeigen, wie man Unit-Tests schreibt, es soll erklärt werden, wie ein EJB getestet werden kann. Wenn man verstanden hat, wie das funktioniert, kann jeder Leser als Übungsaufgabe Tests für die anderen Methoden des EJBs schreiben. ;-)
Um die Klasse UrlManagerBean
remote ansprechen zu können, muss als erstes ein Remote-Interface für das EJB definiert werden. In dem einführenden Beispiel wurde dieses Remote-Interface weggelassen, weil es für das Beispiel nicht benötigt wurde.
Als erstes wird also das Remote-Interface erstellt. Wichtig ist die Annotation @Remote
. Da nur die eine Methode getestet werden soll, wird auch nur diese in das Interface aufgenommen.
package org.hameister.urlmanager; import java.util.List; import javax.ejb.Remote; /** * * @author Hameister */ @Remote public interface UrlManagerBeanRemoteInterface { public List<String> getUrls(); }
Anschließend wird die Klasse UrlManagerBean
um implements UrlManagerBeanRemoteInterface
erweitert.
package org.hameister.urlmanager; import java.util.ArrayList; import java.util.List; import javax.ejb.Stateless; import javax.inject.Named; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; /** * * @author Hameister */ //@Interceptors(UrlManagerInterceptor.class) @Stateless @Named @Path("/urlmanager") public class UrlManagerBean implements UrlManagerBeanRemoteInterface { @PersistenceContext EntityManager em; public void addUrl(UrlManagerEntity entity) { System.out.println("Added"); em.persist(entity); } public List<String> getUrls() { List<String> urls = new ArrayList<String>(); Query query = em.createQuery("SELECT url FROM UrlManagerEntity url"); List<UrlManagerEntity> resultList = query.getResultList(); for(UrlManagerEntity entity : resultList) { urls.add(entity.getUrl()); } return urls; } ... }
Nun ist die eine Methode remote erreichbar.
Als nächstes wird eine Test-Klasse angelegt. Wenn man NetBeans verwendet, dann öffnet man den New File-Wizard mit Strg+N oder cmd+N und wählt im Schritt Choose file type bei Categories den Wert Unit Tests aus. Unter File types selektiert man JUnit Test und spring mit Next weiter.

Anschließend wird als Class name der Wert UrlManagerBeanTest eingetragen. Als Location sollte immer Test Packages gewählt sein, damit die Test-Klassen in einem eigenen source-Ordner, getrennt von dem zu testenden Code, liegen. Als Package wird der Wert org.hameister.urlmanager eintragen. Abschließend wird der Dialog mit Finish geschlossen.

Die generierte Klasse wird so angepaßt, dass sie folgendermaßen aussieht:
package org.hameister.urlmanager; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.embeddable.EJBContainer; import javax.naming.Context; import javax.naming.NamingException; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; /** * * @author Hameister */ public class UrlManagerBeanTest { private static EJBContainer container; private static Context ctx; public UrlManagerBeanTest() { } @BeforeClass public static void setUpClass() { container = EJBContainer.createEJBContainer(); ctx = container.getContext(); } @AfterClass public static void tearDownClass() { container.close(); } @Test public void getUrlsGreaterZero(){ try { UrlManagerBeanRemoteInterface bean = (UrlManagerBeanRemoteInterface)ctx.lookup("java:global/classes/UrlManagerBean"); List<String> result = bean.getUrls(); assertNotNull("Result list with URLs must not be null,", result); assertTrue("Expected more than one url.", result.size()>0); } catch (NamingException ex) { Logger.getLogger(UrlManagerBeanTest.class.getName()).log(Level.SEVERE, null, ex); } } }
Vor dem Start des Tests wird ein EJB-Container mit EJBContainer.createEJBContainer()
in der Methode setUpClass()
erstellt. Wichtig dabei ist die Annotation @BeforeClass
, die dafür sorgt, dass der Container beim Start des Tests erstellt ist und der Context verwendet werden kann.
Nach den Tests wird in der Methode tearDownClass()
der Container wieder geschlossen. Auch hier ist die Annotation @AfterClass
zu beachten.
Der eigentliche Test ist in der Methode getUrlsGreaterZero()
zu finden. Der Test wird durch die Annotation @Test
kenntlich gemacht.
Als erstes wird mittels des Context ctx
eine Suche nach der EJB durchgeführt. Der JNDI-Name ist seit EJB 3.1 standardisiert. Er setzt sich folgendermaßen zusammen:
java:global[/<app-name>]/<module-name>/<bean-name>/[!<fully-qualified-package-name>]
In dem Beispiel kann eine vereinfachte Variante verwendet werden, weil einfach gegen die übersetzten Klassen im Verzeichnis classes
getestet wird. Bei NetBeans sind die Klassen unter [NETBEANS-WORKSPACE]/URLManager/build/web/WEB-INF/classes
zu finden.
Der Rückgabewert des lookup
ist ein Objekt vom Typ UrlManagerBeanRemoteInterface
. Dieses bietet genau die eine Methode an, die wir weiter oben in dem Interface veröffentlicht haben. Also können einfach mit bean.getUrls()
die Urls aus der Datenbank abgefragt werden. Anschließend kann mit JUnit-Standardmitteln überprüft werden, ob das Ergebnis den Erwartungen entspricht.
Für den Test muß der Glassfish-Server nicht hochgefahren werden. Allerdings ist zu beachten, dass die Derby-Datenbank manuell gestartet werden muss.
Wenn die Datenbank hochgefahren wurde, kann der Test einfach mit Test File
oder Run File
gestartet werden.
Falls es zu folgender Exception kommen sollte, dann kann es daran liegen, dass der Glassfish sich an der Datei beans.xml
stört. Wenn man diese aus dem Classpath entfernt oder verschiebt/löscht, dann läuft der Test durch.
WARNUNG: Error while trying to load Bean Class org.apache.tools.ant.util.LineOrientedOutputStreamRedirector : java.lang.VerifyError: class org.apache.tools.ant.util.LineOrientedOutputStreamRedirector overrides final method flush.()V 26.09.2012 20:16:54 org.glassfish.api.ActionReport failure SCHWERWIEGEND: Exception while loading the app 26.09.2012 20:16:54 org.glassfish.deployment.admin.DeployCommand execute SCHWERWIEGEND: Exception while loading the app : tried to access class javax.xml.bind.ContextFinder from class javax.xml.bind.ContextFinder$2 java.lang.IllegalAccessError: tried to access class javax.xml.bind.ContextFinder from class javax.xml.bind.ContextFinder$2 at java.lang.Class.getEnclosingMethod0(Native Method) at java.lang.Class.getEnclosingMethodInfo(Class.java:929) at java.lang.Class.getEnclosingMethodInfo(Class.java:929) at java.lang.Class.getEnclosingClass(Class.java:1081) at java.lang.Class.getSimpleBinaryName(Class.java:1220) at java.lang.Class.isMemberClass(Class.java:1210) at org.jboss.weld.util.reflection.Reflections.isNonStaticInnerClass(Reflections.java:139) at org.jboss.weld.bootstrap.BeanDeployer.addClass(BeanDeployer.java:78) at org.jboss.weld.bootstrap.BeanDeployer.addClasses(BeanDeployer.java:123) at org.jboss.weld.bootstrap.BeanDeployment.createBeans(BeanDeployment.java:184) at org.jboss.weld.bootstrap.WeldBootstrap.deployBeans(WeldBootstrap.java:350) at org.glassfish.weld.WeldDeployer.event(WeldDeployer.java:179) at org.glassfish.kernel.event.EventsImpl.send(EventsImpl.java:128) at org.glassfish.internal.data.ApplicationInfo.load(ApplicationInfo.java:277) at com.sun.enterprise.v3.server.ApplicationLifecycle.deploy(ApplicationLifecycle.java:460) at com.sun.enterprise.v3.server.ApplicationLifecycle.deploy(ApplicationLifecycle.java:240) at org.glassfish.deployment.admin.DeployCommand.execute(DeployCommand.java:389) at com.sun.enterprise.v3.admin.CommandRunnerImpl$1.execute(CommandRunnerImpl.java:348) at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:363) at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:1085) at com.sun.enterprise.v3.admin.CommandRunnerImpl.access$1200(CommandRunnerImpl.java:95) at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1291) at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1259) at com.sun.enterprise.admin.cli.embeddable.DeployerImpl.deploy(DeployerImpl.java:129) at com.sun.enterprise.admin.cli.embeddable.DeployerImpl.deploy(DeployerImpl.java:105) at org.glassfish.ejb.embedded.EJBContainerImpl.deploy(EJBContainerImpl.java:140) at org.glassfish.ejb.embedded.EJBContainerProviderImpl.createEJBContainer(EJBContainerProviderImpl.java:134) at javax.ejb.embeddable.EJBContainer.createEJBContainer(EJBContainer.java:127) at javax.ejb.embeddable.EJBContainer.createEJBContainer(EJBContainer.java:102) at de.hameister.bean.JHSessionBeanTest.<init>(JHSessionBeanTest.java:26) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27) at java.lang.reflect.Constructor.newInstance(Constructor.java:513) at org.junit.runners.BlockJUnit4ClassRunner.createTest(BlockJUnit4ClassRunner.java:187) at org.junit.runners.BlockJUnit4ClassRunner$1.runReflectiveCall(BlockJUnit4ClassRunner.java:236) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.BlockJUnit4ClassRunner.methodBlock(BlockJUnit4ClassRunner.java:233) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:30) at org.junit.runners.ParentRunner.run(ParentRunner.java:300) at junit.framework.JUnit4TestAdapter.run(JUnit4TestAdapter.java:39) at org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner.run(JUnitTestRunner.java:520) at org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner.launch(JUnitTestRunner.java:1060) at org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner.main(JUnitTestRunner.java:911) 26.09.2012 20:16:54 org.glassfish.ejb.embedded.EJBContainerProviderImpl createEJBContainer
Bei der Verwendung des Embeddable-Containers sollte aber nicht vergessen werden, dass nicht nur diese Unit-Tests oder Modul-Tests notwendig sind, um die Funktionsfähigkeit einer Anwendung sicherzustellen. Es muß auch an die Integrationstest gedacht werden. Diese sollten auf einem "richtigen" Application-Server durchgeführt werden.
Weitere Informationen
Weitere Information zu EJB 3.1 und Embeddable Container findet man in der Enterprise JavaBean-Spezifikation 3.1 (JSR-318). Hier liegt das PDF dazu.
- JEE6-Tutorial
- EJB-Tutorial
- TimerService
- Interceptors
- Bean-Validation
- REST-Schnittstelle
- Remote Zugriff auf EJBs mit einem Standalone-Java-Client