Über unsMediaKontaktImpressum
Carlos Barragan 22. September 2015

Bean-Testing – TDD für Java EE-Anwendungen ohne Mocks und Application Server

Unit-Tests sind der Kern jeder Strategie, qualitativ hochwertige Software herzustellen. Im Umfeld von Java EE erschwert allerdings die Notwendigkeit einer Ablaufumgebung das Unit-Testing. Es gibt zwar verschiedene Ansätze für dieses Problem, aber entweder sind diese aufwändiger als gewohnt oder die Ausführungsdauer der Tests liefert nicht die übliche Feedback-Geschwindigkeit. Beim Bean-Testing werden ganze Beans inklusive ihrer Abhängigkeiten und ohne Application Server getestet. So werden Fehler vor Integrationtests entdeckt.

Das Verfahren verwendet CDI (Context and Dependency Injection) um die Laufzeitumgebung eines Java EE-Application Servers zu simulieren. Transaction Propagation, Dependency Injection, Interceptoren usw. sind auf der Java SE-Umgebung vorhanden. Bean-Testing ist somit Application Server unabhängig.

Beim Bean-Testing werden Fehler entdeckt, die normalerweise bei Integrationstests auftreten. Das führt zu schnellem Feedback beim Testing und man kann sogar TDD (Test Driven Development) ohne Mocks einsetzen. Bean-Testing ist kein Prototyp und wird bereits produktiv in unterschiedlichen Projekten eingesetzt.

Es ist bereits bekannt, dass Tests eine große Rolle hinsichtlich der Qualität in einem Enterprise Software-Projekt spielen. Unter Anwendung der heutigen agilen Verfahren ist der Testumfang sowie die Testtiefe noch wichtiger, denn es muss sichergestellt werden, dass das System bei jeder Änderung am Code noch weiter funktioniert. Dafür gibt es verschiedene Tools und Prozesse, die das Testing vereinfachen.

Im Java EE-Umfeld werden Unit-Tests in Verbindung mit Mocks sehr häufig eingesetzt. Dadurch wird die Logik auf Methoden-Ebene getestet. Zum Testen der Komponenten in der gesamten  Anwendung werden Integrationstests implementiert. Dafür wird die Anwendung gebaut und auf einen Application Server installiert, dann werden die Tests ausgeführt. Das Feedback – also die Zeit, die die Tests benötigen, um ein Ergebnis zurückzugeben – dauert natürlich eine gewisse Zeit, normalerweise mehrere Sekunden oder sogar Minuten. Dies sind soweit die Möglichkeiten, die wir zum Testing haben.

Nun stellen wir uns folgendes Szenario vor: Ein Entwickler hat ein paar Änderungen an JPA-Queries sowie an der Persistenzschnittstelle gemacht. Er möchte nun diese Änderungen testen. Anhand der oben genannten Möglichkeiten kann er nur die Anwendung bauen, installieren und danach testen, ob diese Änderungen funktionieren. Falls er sich bei einer Query vertippt hat, funktionieren die Tests nicht und der Entwickler benötigt mehrere Minuten um diesen Fehler zu finden. Es kommt relativ oft vor, dass die Unit-Tests grün sind, aber erst nachdem die Anwendung installiert und gestartet wurde, entdeckt man die Programmierfehler. Das ist zu erwarten, denn Unit-Tests beschränken sich auf Methoden und mocken daher alle externe Dependencies. Deshalb sollte man auch Integrationstests – am besten automatisiert – durchführen. Allerdings werden viele von diesen Tests nur deshalb als Integrationstest implementiert, weil sie Services vom Application Server benötigen, die auf einer Java SE-Umgebung nicht vorhanden sind. Dabei handelt es sich meistens um Dependency Injection und Transaktionen.

Es wäre gut, wenn es ein Framework gäbe, das die meist verwendeten Application Server Services zur Verfügung stellt ohne den Application Server. Genau das bietet das Framework "BeanTest". Allerdings implementiert das Framework selbst keine Funktionalität, sondern verwendet einfach die anderer Frameworks bzw. Spezifikationen wie CDI (Context and Dependency Injection). Wie der Name schon verrät, kümmert sich CDI um Dependency Injection, was bei jeder modernen Java EE-Anwendung unentbehrlich ist. Analog zu anderen Java EE-Spezifikationen hat CDI mehrere Implementierungen. "JBoss Weld" ist in diesem Fall die Standard-Implementierung. Besonders an Weld ist die Möglichkeit, den CDI-Container außerhalb des Application Servers zu verwenden. Man kann die kompletten CDI-Funktionalitäten wie Interceptoren, DI, Producer-Methoden, Extensions, etc. ohne Application Server verwenden. CDI-Extensions ist ein mächtiges Feature, das anhand eines SPI (Service Provider Interface) ermöglicht, das CDI-API zu erweitern. Man kann anhand Extensions den DI-Mechanismus anpassen oder andere Frameworks mit CDI integrieren, und alles sogar portabel.

Die Logik einer Java EE-Anwendung besteht aus der Interaktion von mehreren EJBs. Ein EJB greift auf andere EJBs zu mit der Injektion: @EJB. Der EJB-Container besorgt die entsprechende EJB-Instanzen mit den dazugehörigen Services wie Transaktionen oder Security. Der CDI-Container kann in der aktuellen Java EE Version EJB-Instanzen noch nicht erzeugen. Das heißt, der CDI-Container ignoriert die Annotation @EJB und genau an dieser Stelle wird die CDI-Extension eingesetzt. Durch eine CDI-Extension ist es möglich, die EJB Injection (@EJB) in CDI-Injection Points (@Inject) zu konvertieren und alles ohne ByteCode Manipulation oder Reflection, denn es wird das sogenannte "Bean Meta Model" dazu verwendet.

So sieht die BeanTestExtension aus, die die Konvertierung durchführt (Javadoc wird aus Gründen der Lesbarkeit nicht angezeigt):


package info.novatec.beantest.extension;

import info.novatec.beantest.transactions.Transactional;
import javax.ejb.EJB;
import javax.ejb.MessageDriven;
import javax.ejb.Singleton;
import javax.ejb.Stateless;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.spi.WithAnnotations;
import javax.inject.Inject;
import javax.interceptor.Interceptor;
import org.apache.deltaspike.core.util.metadata.builder.AnnotatedTypeBuilder;

public class BeanTestExtension implements Extension { 
    
    public <x> void processInjectionTarget(@Observes @WithAnnotations({Stateless.class, MessageDriven.class, Interceptor.class, Singleton.class}) ProcessAnnotatedType<x> pat) {
        if (pat.getAnnotatedType().isAnnotationPresent(Stateless.class) || pat.getAnnotatedType().isAnnotationPresent(MessageDriven.class)) {
            modifiyAnnotatedTypeMetadata(pat);
        } else if (pat.getAnnotatedType().isAnnotationPresent(Interceptor.class)) {
            processInterceptorDependencies(pat);
        } else if(pat.getAnnotatedType().isAnnotationPresent(Singleton.class)) {
            addApplicationScopedAndTransactionalToSingleton(pat);
        }
    }
    
    private <x> void addApplicationScopedAndTransactionalToSingleton(ProcessAnnotatedType<x> pat) {
        AnnotatedType<x> at = pat.getAnnotatedType();
        AnnotatedTypeBuilder<x> builder = new AnnotatedTypeBuilder().readFromType(at);        
        builder.addToClass(AnnotationInstances.APPLICATION_SCOPED).addToClass(AnnotationInstances.TRANSACTIONAL);        
        InjectionHelper.addInjectAnnotation(at, builder);        
        pat.setAnnotatedType(builder.create());
    }

    private <x> void modifiyAnnotatedTypeMetadata(ProcessAnnotatedType<x> pat) {
        AnnotatedType<x> at = pat.getAnnotatedType();        
        AnnotatedTypeBuilder<x> builder = new AnnotatedTypeBuilder<x>().readFromType(at);
        builder.addToClass(AnnotationInstances.TRANSACTIONAL).addToClass(AnnotationInstances.REQUEST_SCOPED);
        InjectionHelper.addInjectAnnotation(at, builder);
        //Set the wrapper instead the actual annotated type
        pat.setAnnotatedType(builder.create());

    }
    
    private <x> void processInterceptorDependencies(ProcessAnnotatedType<x> pat) {
        AnnotatedTypeBuilder<x> builder = new AnnotatedTypeBuilder<x>().readFromType(pat.getAnnotatedType());
        InjectionHelper.addInjectAnnotation(pat.getAnnotatedType(), builder);
        pat.setAnnotatedType(builder.create());
    }

Wie man hier sehen kann, verwendet die Extension bis auf die Klasse AnnotatedTypeBuilder nur Klassen aus der CDI-Spezifikation. Die Klasse AnnotatedTypeBuilder kommt aus dem Apache Deltaspike-Framework und mit dieser Klasse wird das Bean-Meta-Model angepasst. Das modifizierte Meta-Model wird dann an Stelle des ursprünglichen Bean bei pat.setAnnotatedType() gesetzt. Deltaspike ist eine Art "CDI-Toolbox", durch die viel boilerplate Code gespart werden kann. Darüber hinaus bietet Deltaspike CDI-Extensions an, um zusätzliche Funktionalität bei einer Java EE-Anwendung zur Verfügung zu stellen [1].

Die Klasse InjectionHelper fügt lediglich die @Inject Annotation anhand des Builders hinzu, falls der entsprechende "Member" eine der Annotationen @EJB, @PersistenceContext oder @Resource enthält. Es werden weder Reflection noch ByteCode-Manipulation für das Hinzufügen der Annotation verwendet.


public final class InjectionHelper {
    
    private static final Set<Class<? extends Annotation>> JAVA_EE_ANNOTATIONS = createJavaEEAnnotationSet();
            
    private static Set<Class<? extends Annotation>> createJavaEEAnnotationSet() {
        Set<Class<? extends Annotation>> javaEEAnnotations = new HashSet<Class<? extends Annotation>>();
        javaEEAnnotations.add(Resource.class);
        javaEEAnnotations.add(EJB.class);
        javaEEAnnotations.add(PersistenceContext.class);
        return Collections.unmodifiableSet(javaEEAnnotations);
    }
    
    private InjectionHelper() {
        // Empty on purpose.
    }

    public static <x>  boolean shouldInjectionAnnotationBeAddedToMember(AnnotatedMember<? super X> member) {
        return !member.isAnnotationPresent(Inject.class) && hasJavaEEAnnotations(member);
    }
    
    private static <x> boolean hasJavaEEAnnotations(AnnotatedMember<? super X> member) {
         for(Class<? extends Annotation>> javaEEannotation : JAVA_EE_ANNOTATIONS) {
             if (member.isAnnotationPresent(javaEEannotation)) {
                 return true;
             }
         }
         return false;
    }


    public static <x> void addInjectAnnotation(final AnnotatedType<x> annotatedType, AnnotatedTypeBuilder<x> builder) {
        for (AnnotatedField<? super X> field : annotatedType.getFields()) {
            if (shouldInjectionAnnotationBeAddedToMember(field)) {
                builder.addToField(field, AnnotationInstances.INJECT);
            }
        }
        for (AnnotatedMethod<? super X> method : annotatedType.getMethods()) {
            if (shouldInjectionAnnotationBeAddedToMember(method)) {
                builder.addToMethod(method,  AnnotationInstances.INJECT);
            }
        }
    }
}

In der Extension kann man auch sehen, dass @RequestScoped und @Transactional dem Bean auf Klasse-Ebene hinzugefügt wird. Die @RequestScoped ist eine Standard-CDI-Annotation um den Scope des Beans zu definieren – in diesem Fall Request. @Transactional ist ein "InterceptorBinding". Dies ist ein CDI-Feature, um einen Interceptor mit einer Klasse oder einer Methode zu verbinden. Dabei handelt es sich um Transaktionen mit dem entsprechenden Interceptor: TransactionalInterceptor.


@Interceptor
@Transactional
public class TransactionalInterceptor {
    
    /**
     * Exceptions that should not cause the transaction to rollback according to Java EE Documentation. 
     * (ht tp://docs.oracle.com/javaee/6/api/javax/persistence/PersistenceException.html)
     */
    private static final Set<Class<?>> NO_ROLLBACK_EXCEPTIONS=new HashSet<Class<?>>(Arrays.asList(
            NonUniqueResultException.class,
            NoResultException.class,
            QueryTimeoutException.class,
            LockTimeoutException.class));
    

    @Inject
    @PersistenceContext
    EntityManager em;

    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalInterceptor.class);

    private static int INTERCEPTOR_COUNTER = 0;

    @AroundInvoke
    public Object manageTransaction(InvocationContext ctx) throws Exception {
        
        EntityTransaction transaction = em.getTransaction();
        if (!transaction.isActive()) {
            transaction.begin();
            LOGGER.debug("Transaction started");
        }

        INTERCEPTOR_COUNTER++;
        Object result = null;
        try {
            result = ctx.proceed();

        } catch (Exception e) {
            if (isFirstInterceptor()) {
                markRollbackTransaction(e);
            }
            throw e;
        } finally {
            processTransaction();
        }

        return result;
    }
    
    
    private void processTransaction() throws Exception {
        EntityTransaction transaction = em.getTransaction();
        try {
            
            if (em.isOpen() && transaction.isActive() && isFirstInterceptor()) {
                if (transaction.getRollbackOnly()) {
                    transaction.rollback();
                    LOGGER.debug("Transaction was rollbacked");
                } else {
                    transaction.commit();
                    LOGGER.debug("Transaction committed");
                }
                em.clear();
            }
        } catch (Exception e) {
            LOGGER.warn("Error when trying to commit transaction: {0}", e);
            throw e;
        } finally {
            INTERCEPTOR_COUNTER--;
        }

    }

    private void markRollbackTransaction(Exception exception) throws Exception {
        try {
            if (em.isOpen() && em.getTransaction().isActive() && shouldExceptionCauseRollback(exception)) {
                em.getTransaction().setRollbackOnly();
            }
        } catch (Exception e) {
            LOGGER.warn("Error when trying to roll back the  transaction: {0}", e);
            throw e;
        }
    }

    private static boolean isFirstInterceptor() {
        return INTERCEPTOR_COUNTER -1 == 0;
    }
    
    private static boolean shouldExceptionCauseRollback(Exception e ) {
        return ! NO_ROLLBACK_EXCEPTIONS.contains(e.getClass());
    }

}

Anhand des TransactionalInterceptor wird eine Transaktion gestartet und an weitere Aufrufe propagiert. Dies ist das gleiche Prinzip bei einem Application Server: Jeder Aufruf wird von einem Application Server Interceptor abgefangen und die Transaktion wird dementsprechend propagiert, committed oder es wird – im Fall eines Fehlers – ein Rollback durchgeführt. Bei dem TransactionalInterceptor handelt sich um ein vereinfachtes Transaction Handling, das jedoch für das Testing ausreichend ist.

Im TransactionalInterceptor ist zu sehen, dass der EntityManager injiziert wird. Der CDI-Container kennt aber die @PersistenceContext Annotation nicht. Daher muss man den EntityManager anhand eines Producers zur Verfügung stellen:


@RequestScoped
public class EntityManagerProducer {
    
    private static final String DEFAULT_BEAN_TEST_PERSISTENCE_UNIT = "beanTestPU";
     
    private EntityManagerFactory emf;
    
    private EntityManager em;

    @PostConstruct
    private void initializeEntityManagerFactory() {
        emf = Persistence.createEntityManagerFactory(DEFAULT_BEAN_TEST_PERSISTENCE_UNIT);
    }
   

    @Produces
    public EntityManager getEntityManager(InjectionPoint ip) {
        PersistenceContext ctx = ip.getAnnotated().getAnnotation(PersistenceContext.class);
        
        if (ctx == null) {
          Member member = ip.getMember();
          if (member instanceof Method) {
            Method method = (Method) member;
            ctx = method.getAnnotation(PersistenceContext.class);
          }
        }

        if (em == null) {
            em = emf.createEntityManager();
        }
        return em;
    }
    // Weitere Hilfsmethoden aus Gründen der Lesbarkeit ausgeblendet

}

Beim EntityManagerProducer wird das normale JPA Bootstrapping-Verfahren verwendet. Dazu muss eine Persistence-Unit namens "beanTestPU" vorhanden sein. Der Entwickler soll diese Persistence-Unit zur Verfügung stellen, indem er sie in die persistence.xml-Datei einträgt. Somit kann man die Datenbank-Verbindung beliebig konfigurieren, wobei es empfehlenswert ist, eine in-memory-Datenbank wie H2 oder Derby zu verwenden. In diesem Fall sollte man die Persistence-Unit so konfigurieren, dass die Datenbank beim JPA-Bootstrapping erstellt wird:

Listing 5

<persistence-unit name="beanTestPU" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>info.novatec.beantest.demo.entities.MyEntity</class>
        <class>info.novatec.beantest.demo.entities.MyEntityWithConstraints</class>
        <properties>
            <property name="javax.persistence.jdbc.url" value="jdbc:derby:memory:myDB;create=true"/>
            <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>  
    </persistence-unit>

In diesem Beispiel wird Hibernate als Persistence Provider verwendet. Der Entwickler kann aber andere Persistence Provider konfigurieren.

Klassen wie TransactionalInterceptor und EntityManagerProducer verwenden lediglich Standard-CDI-Features um ihre Services anbieten zu können. Die CDI-Extension verändert das Bean Meta Model so, dass diese Funktionalität dem CDI-Container bekannt ist. Die Extension wird beim Bootstrapping des CDI-Containers durchgeführt. Der CDI-Container bearbeitet alle Klassen, die im Classpath zu finden sind. Das geschieht vor der Ausführung der Tests. Somit wird sichergestellt, dass die InjectionPoints und Dependencies aufgelöst sind, wenn die Tests ausgeführt werden:

Bean-Test

Nach dieser Transformation anhand der Extension kann der CDI-Container die Dependencies injizieren und die Interceptoren ausführen. Auf diese Art und Weise sind Dependency Injection und Transaction Handling ohne Application Server möglich.

Ein Bean-Test würde wie folgt aussehen:


public class CustomerServiceBeanTest extends BaseBeanTest {

    @Test
    public void shouldFindCustomer() {

        CustomerPersistenceService customerPs = getBean(CustomerPersistenceService.class);

        assertThat(customerPs.findAll(), is(empty()));

        Customer customer = new Customer("John", "123456", CustomerStatus.NORMAL);

        customer = customerPs.save(customer);
        assertThat(customer.getId(), not(0L));

        Customer customerFromDb = customerPs.find(customer.getId());

        assertThat(customerFromDb, is(notNullValue()));
        assertThat(customer.getId(), is(customerFromDb.getId()));
        assertThat(customer.getName(), is(customerFromDb.getName()));
        assertThat(customer.getCustomerId(), is(customerFromDb.getCustomerId()));

    }
}

Wie im obigen Beispiel zu sehen ist, ist ein Bean-Test einem Unit-Test sehr ähnlich. Man könnte sich nun fragen, ob es Sinn macht, alle Unit-Tests dann durch Bean-Tests zu ersetzen. Allerdings ist Bean-Testing kein Ersatz von Unit-Tests. Bean-Testing ist etwas in der Mitte zwischen Unit- und Integrationstests. Unit-Tests sind was Geschwindigkeit betrifft unschlagbar. Mit Unit-Tests kann man Algorithmen sehr gut und schnell testen. Bean-Tests sind gut dafür geeignet, Interaktionen innerhalb eines Moduls (z. B. einem .jar-File) zu testen und Integrationstests sollen die komplette Anwendung von außen testen. Das heißt, Integrationstests sollen durch die öffentliche Schnittstelle (z. B. Rest) die Anwendung testen. Somit erreicht man eine ausgeglichene Test-Pyramide.

Was ist dann der Vorteil von Bean-Tests? Der Vorteil ist, dass man auf unnötige Integrationstests verzichten kann und somit ein schnelleres Feedback beim Testing erreicht. Aus eigener Erfahrung in unterschiedlichen Projekten habe ich beobachtet, dass viele Tests als Integrationstests umgesetzt werden, weil es keine andere Möglichkeit gibt, die Komponenten ohne Application Server zu testen. Besonders geeignet für Bean-Tests ist die Persistenzschnittstelle weil man mit Bean-Testing keinen Application Server benötigt um die Persistenz testen zu können. Das führt dazu, dass kleine Änderungen, z. B. eine Anpassung an einer Query, sofort testbar sind, denn die Anwendung muss nicht auf einen Application Server installiert werden.

Fazit

Wie kann man das selbst ausprobieren? Bean-Test gibt es als Open Source-Projekt mit Apache Lizenz auf GitHub [2]. Das Projekt kann als Maven Dependency zu einem eigenem Projekt hinzugefügt werden. Grundsätzlich kann man mit Bean-Test wie folgt loslegen:

1. Maven Dependency hinzufügen

<dependency>
    <groupId>info.novatec</groupId>
    <artifactId>bean-test</artifactId>
    <version>{currentVersion}</version>
    <scope>test</scope>
</dependency>

2. NovaTec Repo hinzufügen:

<repository>
    <id>Novatec</id>
    <name>Novatec Repository</name>
    <url> repository.novatec-gmbh.de/content/repositories/novatec </url>   
</repository>

3. Leere bean.xml-Datei unter src/test/resources/META-INF anlegen

4. Eine Persistence-Unit mit Namen "beanTestPU" in persistence.xml-Datei eintragen. Hinweis: es muss nicht unbedingt die persistence.xml-Datei sein, die für "Produktion" verwendet wird (src/main/resources). Die Persistence-Unit kann in persistence.xml unter src/test/resoureces/META-INF eingetragen werden.

Es gibt auch ein Demo-Projekt, bei dem man sich Bean-Tests anschauen kann [3].

Wir setzten Bean-Tests nicht nur in Demo-Projekten ein sondern auch in produktiven Projekten bei unterschiedlichen Kunden. Die Komplexität der Projekte variiert von ein paar Modulen bis zu komplexen Projekten; z. B. 15 Module, zwei .ear-Dateien und ein schwerfälliger Application Server.

Ich hoffe, ich konnte Ihr Interesse am Thema Java EE-Testing wecken. Ich bin der Meinung, Bean-Testing ist eine simple, jedoch mächtige Library um besser und schneller testen zu können. Man kann Fehler relativ früh im Entwicklungsprozess entdecken bzw. man kann sogar echte TDD bei Java EE-Anwendungen einsetzen. Ich freue mich sehr über Ihr Feedback zum Thema und zum Projekt. Viel Spaß beim Testing!

Autor

Carlos Barragan

Carlos Barragan arbeitet als Consultant bei der NovaTec Consulting GmbH und beschäftigt sich seit über 10 Jahren mit Enterprise-Anwendungen. An verschiedenen Projekten war er als Software-Entwickler, Berater und Architekt…
>> Weiterlesen
Kommentare (0)

Neuen Kommentar schreiben