Unit-Tests für EJBs mit einem Embeddable-Container - JEE6 - Java Enterprise Edition 6

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.

Create New Project

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.

Create New Project

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.