Startseite

Testen von Java EE Anwendungen mit JUnit

Im Enterprise Umfeld entwickelt man häufig Java Anwendungen, die ihre Funktionen über Web Schnittstellen (REST, SOAP, HTML) exponieren. Die gängigen Applikation-Server (Wildfly, Glassfish, WebLogic) stellen die dazu nötige Laufzeitumgebung bereit.

Das im Java Umfeld übliche JUnit Framework ist in diesem Bereich hilfreich, um einzelne Funktionen direkt auf dem Arbeitsplatz-Rechner des Entwicklers zu testen, ohne die ganze Anwendung in eine Laufzeitumgebung zu installieren. Auf diese Weise kann man Programm-interne Funktionen schnell und einfach testen, sogar wenn das Projekt in unfertigem nicht kompilierbaren Zustand ist.

Um den grundsätzlichen Umgang mit JUnit zu erlernen, empfehle ich das Tutorial von Tutego. In diesem vorliegenden Artikel beschreibe ich, zusätzliche Aspekte, die bei Enterprise Anwendungen dazu kommen.

Demo Projekt

Ich erkläre die Vorgehensweise anhand einer kleinen Java EE 7 Web-Anwendung. Sie erzeugt eine Webseite, welche den Inhalt einer Datenbank-Tabelle darstellt. Außerdem enthält sie unfertigen Code zur Kommunikation mit einem anderen HTTP Server.

Download:

Installation der Laufzeitumgebung

Was ich hier über JUnit berichten möchte ist weitgehend unabhängig von der konkreten Laufzeitumgebung. Für die Demonstration verwende ich:

Der Artikel ist schon etwas älter, aber er trifft auf aktuelle Software Versionen immer noch zu.

Das Java Development Kit soll ganz normal installiert werden, so dass der Befehl java -version die korrekte Version anzeigt. Da der Wildfly Server nicht ständig als Dienst laufen soll, empfiehlt es sich, die ZIP Datei im persönlichen Verzeichnis zu entpacken. Der MariaDB Treiber gehört ins Verzeichnis wildfly-12.0.0.Final/standalone/deployments.

Der Wildfly Server wird durch das Script wildfly-12.0.0.Final/bin/standalone.sh bzw. wildfly-12.0.0.Final/bin/standalone.bat im Kommandozeilen-Fenster gestartet. Zum Stoppen drückt man in diesem Fenster Strg-C.

Manche Linux Distributionen richten bei der Installation der MariaDB ein root Passwort ein. In diesem Fall startet man den MariaDB Client mit dem Befehl mysql -uroot -ppasswort (ohne Leerzeichen hinter -p!) oder mysql -uroot -p. Bei Debian hingegen hat der root User standardmäßig kein Passwort, so dass man dort den Befehl mysql -uroot verwendet. Damit Sie als normaler Linux Benutzer den MariaDB Client benutzen dürfen, muss der Administrator einmal den Befehl usermod -a -G mysql benutzername ausführen.

Erstellen Sie das folgende Datenbank-Schema mit dem MariaDB Client indem Sie folgende Befehle ausführen:

create schema demo;
use demo;

create table friends (id integer primary key auto_increment, name varchar(200));
insert into friends(name) values ('Robert'),('Lisa'),('Julia'),('Ali');

grant all on demo.* to 'user1' identified by 'password1';
flush privileges;
exit;

Danach testen Sie, ob der neue Datenbank-Benutzer funktioniert:

root@stefanspc:~# mysql -Ddemo -uuser1 -ppassword1
Welcome to the MariaDB monitor...

MariaDB [demo]> select * from friends;
+----+--------+
| id | name   |
+----+--------+
|  1 | Robert |
|  2 | Lisa   |
|  3 | Julia  |
|  4 | Ali    |
+----+--------+
4 rows in set (0.00 sec)

MariaDB [demo]>

Jetzt konfigurieren Sie den Wildfly Server so, dass er eine Verbindung zu dieser Datenbank über JNDI Bereit stellt. Diese Verbindung wird später von dem Beispielprogramm benutzt. Beenden Sie dazu den Wildfly Server und öffnen dann die Datei wildfly-12.0.0.Final/standalone/​configuration/standalone.xml in einem Text-Editor. Suchen Sie nach dem Abschnitt subsystem xmlns="urn:jboss:domain:datasources. Dort fügen Sie eine Datenbank-Verbindung ein:

<datasource jndi-name="java:jboss/demoDS" pool-name="demoDS">
    <connection-url>jdbc:mariadb://127.0.0.1:3306/demo</connection-url>
    <driver>mariadb-java-client-2.2.2.jar</driver>
    <driver-class>org.mariadb.jdbc.Driver</driver-class>
    <transaction-isolation>TRANSACTION_READ_COMMITTED<</transaction-isolation>
    <security>
        <user-name>user1</user-name>
        <password>password1</password>
    </security>
</datasource>

Starten Sie den Wildfly Server mit bin/standalone.sh oder bin/standalone.bat, um die Konfiguration zu kontrollieren. Der Server sollte ohne Fehlermeldung starten. Stoppen Sie ihn danach wieder.

Installation der Entwicklungsumgebung

Für dieses Projekt habe ich die NetBeans IDE ausgewählt, weil sie über eine sehr gute Maven Integration verfügt. Das Projekt lässt sich in dieser Entwicklungsumgebung mit minimalem Aufwand starten. Installieren Sie also die

Packen Sie Datei demo.zip irgendwo in ihrem persönlichen Verzeichnis aus und öffnen Sie das Projekt in der Netbeans IDE. Gehen Sie im Menü auf Extras/Server, um dort ihren Wildfly Server hinzuzufügen. Im Feld "Server Configuration" soll aber nicht die vorgeschlagene Datei "standalone-full.xml" verwendet werden, sondern die "standalone.xml".

Danach können Sie das Demo Programm durch Klick auf den grünen Start-Pfeil starten. Öffnen Sie im Web Browser die Seite http://localhost:8080/demo-1.0-SNAPSHOT/ falls das nicht automatisch passiert:

Das Demo Projekte sollte nun bei ihnen laufen und die gezeigte Webseite erzeugen.

Beschreibung des Demo Projektes

Schauen wir uns das Projekt kurz an:

Das DemoServlet ist in dieser Anwendung die Darstellungs-Schicht. Sie stellt die Liste der Freunde als HTML Seite dar:

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/")
public class DemoServlet extends HttpServlet {
    
    @Inject
    private BusinessLogic businessLogic;

    protected void processRequest(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet DemoServlet</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("List of friends:");
            out.println("<ul>");
            
            List<Friend> list = businessLogic.listAllFriends();
            for (Friend friend : list) {
                out.println("  <li>"+friend.getName()+"</li>");
            }
            
            out.println("</ul>");
            out.println("</body>");
            out.println("</html>");
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
        processRequest(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
        processRequest(request, response);
    }
}

Anstelle dieses Servlets hätte ich ebenso eine Java Server Page (JSP) oder ein Facelet (JSF) verwenden können.

Die BusinessLogic Klasse enthält die Geschäftslogik der Anwendung. Eine echte Enterprise Anwendung hat ihre Geschäftslogik üblicherweise über zahlreiche Klassen verteilt, die teilweise voneinander abhängen. Das ist der Teil, um den sich dieser Artikel hauptsächlich dreht:

import java.io.IOException;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;

@ApplicationScoped
public class BusinessLogic {
    
    @PersistenceUnit(unitName = "demoPU")
    private EntityManagerFactory emFactory;
    
    public List<Friend> listAllFriends()
    {
        EntityManager entityManager=emFactory.createEntityManager();
        entityManager.getTransaction().begin();
        List<Friend> result=entityManager.createQuery("from Friend f").getResultList();
        return result;
    }
    
    @Inject
    private HttpInterface httpInterface;
    
    public String getStefansHomepage()
    {
        try {
            return httpInterface.readFromUrl("http://stefanfrings.de/index.html");
        } catch (IOException e) {
            return "error "+e.toString();
        }
    }
}

Die Funktion listAllFriends() holt mittels Java Persistence API (JPA) eine Liste von allen Freunden aus der Datenbank. Die Funktion getStefansHomepage() ruft meine Homepage auf und liefert sie als String zurück.

Die HttpInterface Klasse implementiert die Schnittstelle zu meinem Webserver. Sie soll hier als minimales Beispiel für eine externe Server-zu-Server Kommunikation dienen, was in Enterprise Anwendungen häufig vor kommt.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import javax.enterprise.context.RequestScoped;

@RequestScoped
public class HttpInterface {
    
    public String readFromUrl(String url) throws IOException {
        URLConnection conn = new URL(url).openConnection();
        try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)
        )) {
            return reader.lines().collect(Collectors.joining("\n"));
        }
    }    
}

Die Friend Klasse repräsentiert eine Zeile von der Datenbank Tabelle "friends":

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "friends")
public class Friend implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "name")
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Die Persistence Units werden in der Datei persistence.xml konfiguriert. Eine Persistence Unit steht für die Verbindung zu einem Datenbank-Schema:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 
    xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence 
    http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    
    <persistence-unit name="demoPU" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <non-jta-data-source>java:/jboss/demoDS</non-jta-data-source>
        <class>de.stefanfrings.demo.Friend</class>
        <!-- hier könnten mehrere Klassen stehen -->
    </persistence-unit>
    
</persistence>

Der Wildfly Server stellt die Verbindung zur Datenbank als JNDI Datasource "demoPU" bereit, weil er in standalone.xml so konfiguriert wurde:

<datasource jndi-name="java:jboss/demoDS" pool-name="demoDS">
    <connection-url>jdbc:mariadb://127.0.0.1:3306/demo</connection-url>
    <driver>mariadb-java-client-2.2.2.jar</driver>
    <driver-class>org.mariadb.jdbc.Driver</driver-class>
    <transaction-isolation>TRANSACTION_READ_COMMITTED<</transaction-isolation>
    <security>
        <user-name>user1</user-name>
        <password>password1</password>
    </security>
</datasource>

Der Zugriff auf die Datenbank Tabelle "friends" ist also insgesamt so realisiert:

Web Browser ⟶ DemoServlet ⟶ BusinessLogic ⟶ JPA EntityManager ⟶ JPA PersistenceUnit "demoPU" ⟶ Wildfly JNDI DataSource "demoDS" ⟶ JDBC Treiber ⟶ MariaDB Schema "demo"

Testen mit JUnit

Jetzt kommen wir zum eigentlichen Thema dieses Artikels: Das Testen der Geschäftslogik mit JUnit.

Fügen Sie in der Datei pom.xml folgende Abhängigkeit hinzu:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

Dann erstellen Sie unter src/test/java/de/stefanfrings/demo die neue Klasse BusinessLogicTest.java:

import java.util.List;
import javax.inject.Inject;
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class BusinessLogicTest {
    
    @Inject 
    private BusinessLogic businessLogic;
    
    @Test
    public void testDbAccess() {
        List<Friend> list = businessLogic.listAllFriends();
        assertEquals(list.size(),4);
    }
}

Diese Klasse soll die Geschäftslogik der Enterprise Anwendung testen. Versuchen Sie, den Test auszuführen: Rechte Maustaste im Text-Editor, dann den Befehl "Datei testen" wählen.

Das funktioniert nicht, weil zwei Sachen fehlen: Die JNDI Datenquelle und die Dependency Injection.

JNDI Verbindung zur Datenbank

Die JNDI Schnittstelle zur Datenbank, welche normalerweise vom Applikations-Server bereitgestellt wird, kann durch Simple-JNDI ersetzt werden:

Web Browser ⟶ DemoServlet ⟶ BusinessLogic ⟶ JPA EntityManager ⟶ JPA PersistenceUnit "demoPU" ⟶ Simple-JNDI DataSource "demoDS" ⟶ JDBC Treiber ⟶ MariaDB Schema "demo"

Dazu fügen Sie folgende Einträge in die Datei pom.xml ein:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.1.10.Final</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>2.2.2</version>
    <scope>test</scope>
</dependency>        

<dependency>
    <groupId>com.github.h-thurow</groupId>
    <artifactId>simple-jndi</artifactId>
    <version>0.17.2</version>
    <scope>test</scope>
</dependency>

Legen Sie die Datei src/test/resources/jndi.properties mit folgendem Inhalt an:

java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
org.osjava.sj.root=config/
org.osjava.sj.jndi.shared=true
org.osjava.sj.delimiter=/
org.osjava.sj.space=java:/jboss

Die erste Zeile wird von Java selbst ausgewertet. Sie stellt ein, dass Simple-JNDI als Implementierung für JNDI benutzt werden soll. Die anderen Zeilen werden von Simple-JNDI ausgewertet:

Legen Sie auch die Datei {projektverzeichnis}/config/demoDS.properties mit folgendem Inhalt an, um die JDBC Verbindung zu konfigurieren:

type=javax.sql.DataSource
driver=org.mariadb.jdbc.Driver
url=jdbc:mariadb://127.0.0.1:3306/demo
user=user1
password=password1

SimpleJNDI erstellt somit eine Instanz des JDBC Treibers, konfiguriert sie und stellt sie dann im InitialContext unter dem Namen java:/jboss/demoDS bereit. Damit ersetzt sie die Datenquelle, die sonst der Wildlfy Applikations-Server bereitstellen würde. Man kann das so kontrollieren:

InitialContext ctx=new InitialContext();
DataSource ds=(DataSource) ctx.lookup("java:/jboss/demoDS");

Wir wollen aber nicht direkt mit der DataSource Instanz des JDBC Treibers arbeiten, sondern weiterhin JPA benutzen.

In der BusinessLogic Klasse müssen Sie die Annotation @PersistenceUnit ersetzen, weil sie außerhalb des Applikations-Servers keine Funktion hat.

import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

@ApplicationScoped
public class BusinessLogic {
    
    //@PersistenceUnit(unitName = "demoPU")
    private static final EntityManagerFactory emFactory =
        Persistence.createEntityManagerFactory("demoPU");

    public List<Friend> listAllFriends()
    {
        EntityManager entityManager=emFactory.createEntityManager();
        entityManager.getTransaction().begin();
        List<Friend> result=entityManager.createQuery("from Friend f").getResultList();
        return result;
    }
    ...
}

Innerhalb einer Anwendung soll für jedes Datenbank-Schema möglichst genau eine Instanz der EntityManagerFactory existieren, da das Erzeugen unnötiger Instanzen recht teuer ist. Bei den EntityManagern ist es umgekehrt: man soll für jede einzelne Datenbank-Transaktion einen neuen EntityManager verwenden.

Jetzt müssen wir noch (vorläufig) die @Inject Annotation heraus nehmen, um die bisherigen Änderungen testen zu können:

import java.util.List;
import javax.inject.Inject;
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class BusinessLogicTest {
    
    //@Inject 
    //BusinessLogic businessLogic;
    BusinessLogic businessLogic = new BusinessLogic();
    
    @Test
    public void testDbAccess() {
        List<Friend> list = businessLogic.listAllFriends();
        assertEquals(list.size(),4);
    }
}

Sie können den JUnit Test jetzt erfolgreich ausführen.

CDI Dependency Injection

Der CdiTestRunner aus dem Apache Deltaspike Projekt ermöglicht die Verwendung von CDI in einem JUnit Test. Dazu sind die folgenden Einträge in der pom.xml nötig:

<dependency>
    <groupId>org.apache.deltaspike.modules</groupId>
    <artifactId>deltaspike-test-control-module-api</artifactId>
    <version>1.7.2</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.apache.deltaspike.modules</groupId>
    <artifactId>deltaspike-test-control-module-impl</artifactId>
    <version>1.7.2</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.apache.deltaspike.cdictrl</groupId>
    <artifactId>deltaspike-cdictrl-weld</artifactId>
    <version>1.7.2</version>
    <scope>test</scope>
</dependency>  

<dependency>
    <groupId>org.jboss.weld.se</groupId>
    <artifactId>weld-se</artifactId>
    <version>2.4.6.Final</version>
    <scope>test</scope>
</dependency>

Da der Wildfly Server Weld benutzt, verwende ich es hier ebenfalls. In der Dokumentation von Deltaspike werden Sie weitere Alternativen finden.

Damit der CDI Container die Klassen des Projektes überhaupt zum Injecten in Betracht zieht, muss die Datei src/main/resources/META-INF/beans.xml existieren:

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
       http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       version="1.2" bean-discovery-mode="annotated">
</beans>

Legen Sie außerdem eine leere Datei src/test/resources/META-INF/apache-deltaspike_test-container.properties an, um Warnmeldungen zu vermeiden.

Nun kann die @Inject Annotation in der Test-Klasse wieder reaktiviert werden:

import java.util.List;
import javax.inject.Inject;
import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(CdiTestRunner.class)
public class BusinessLogicTest {
    
    @Inject 
    BusinessLogic businessLogic;
    
    @Test
    public void testDbAccess() {
        List<Friend> list = businessLogic.listAllFriends();
        assertEquals(list.size(),4);
    }
}

Probieren Sie es aus, es funktioniert jetzt.

Mocking

Neben der bereits gezeigten Webseite enthält diese Enterprise Anwendung auch eine Server-zu-Server Kommunikation, die jedoch noch nicht benutzt wird. Das ist eine ganz typische Situation während der Entwicklung. Mit Hilfe von JUnit kann der Entwickler solche unfertigen Programmteile sehr einfach testen.

Fügen Sie dazu zur Klasse BusinessLogicTest den folgenden Code hinzu:

    @Test
    public void testHttpCommunication() {
        String content=businessLogic.getStefansHomepage();
        assertTrue(content.contains("Stefan"));
    }

In der Entwicklungsumgebung kann man diesen Test einzeln ausführen, indem man mit der rechten Maustaste auf ihren Namen klickt und dann "Run focused Test Method" anklickt.

Ich habe ihnen das als Vorbereitung gezeigt, um den Sinn von Mocking zu verdeutlichen: Dieser Test setzt voraus, dass der Server von meiner Homepage erreichbar ist und funktioniert. Glücklicherweise ist das fast immer der Fall, doch nun stellt sich die Frage, wie man denn die Reaktion auf Fehler testen kann. Der markierte Code in der BusinessLogic kann nur getestet werden, indem man gezielt eine Kommunikations-Störung simuliert:

    public String getStefansHomepage()
    {
        try {
            return httpInterface.readFromUrl("http://stefanfrings.de/index.html");
        } catch (IOException e) {
            return "error "+e.toString();
        }
    }

Um dies zu tun, werden wir eine spezielle Version der HttpInterface Klasse erzeugen, die wir "Mock" nennen. Sie soll gar nicht mehr mit meinem Webserver kommunizieren, sondern nur simulieren. In diesem konkreten Fall soll sie einen Fehler simulieren.

Dazu werden zwei weitere Abhängigkeiten in der pom.xml benötigt:

<dependency>
    <groupId>org.apache.deltaspike.core</groupId>
    <artifactId>deltaspike-core-api</artifactId>
    <version>1.7.2</version>
</dependency>

<dependency>
    <groupId>org.apache.deltaspike.core</groupId>
    <artifactId>deltaspike-core-impl</artifactId>
    <version>1.7.2</version>
    <scope>runtime</scope>
</dependency>

Legen Sie die Datei src/test/resources/META-INF/apache-deltaspike.properties an, mit folgendem Inhalt:

deltaspike.testcontrol.mock-support.allow_mocked_beans=true
deltaspike.testcontrol.mock-support.allow_mocked_producers=true

Und jetzt können Sie zwei weitere Testfälle schreiben, die mit Hilfe von zwei Mocks unterschiedliche Fehlerfälle simulieren:

    @Inject
    private DynamicMockManager mockManager;
    
    @Test
    public void testHttpCommunicationWithMock() throws Exception {
        
        mockManager.addMock(new HttpInterface() {
            @Override
            public String readFromUrl(String url) throws IOException {
                throw new IOException(
                    "Server returned HTTP response code: 500 for URL: "+url);
            }
        });
        
        String content=businessLogic.getStefansHomepage();
        assertTrue(content.contains("error"));
        assertTrue(content.contains("HTTP response code: 500"));
    } 
    
    @Test
    public void testHttpCommunicationWithMock2() throws Exception {
        
        mockManager.addMock(new HttpInterface() {
            @Override
            public String readFromUrl(String url) throws IOException {
                return "Sorry, this page is under construction";
            }
        });
        
        String content=businessLogic.getStefansHomepage();
        assertTrue(content.contains("under construction"));
    } 

Beide Mocks sind Nachfahren von der HttpInterface Klasse, bei denen die Methode readFromUrl() überschrieben wurde. Somit ersetzen sie die tatsächliche HTTP Kommunikation durch eine Simulation. Mit dem DynamicMockManager wird dafür gesorgt, dass dieser Mock anstelle der normalen Instanz injiziert wird.

Wenn Sie nun in der Klasse HttpInterface die Annotation @RequestScoped auf @ApplicationScoped ändern, funktioniert diese Testklasse nicht mehr korrekt. Zwar können Sie noch jeden Testfall einzeln ausführen, aber sobald Sie die ganze Klasse am Stück ausführen, kommt es zu unerwarteten Fehlern:

Der Grund dafür ist, dass die @ApplicationScoped Annotation dafür sorgt, dass dieses Bean zur Laufzeit der Anwendung nur einmal instantiiert und injiziert wird. Wir brauchen aber eine frische Instanz für jeden Testfall damit immer der jeweils definierte Mock verwendet wird. Dies erreicht man, indem man die gerade aktuelle Instanz vor jedem Testfall aus dem CDI Kontext löscht:

    @Before
    public void setup() {
        HttpInterface i=CDI.current().select(HttpInterface.class).get();
        CDI.current().destroy(i);
    }

Dadurch wird praktisch der Scope vom HttpInterface aufgehoben. Jeder Testfall bekommt nun eine eigene frische Instanz.

Logging

Manchmal sind die Fehlermeldungen und Stack Trace nicht ausreichend, um die Ursache eines Fehlers zu erkennen. Um zusätzliche Log-Meldungen zu erhalten, füge ich in die pom.xml zwei weitere Dependencies ein:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
    <scope>test</scope>
</dependency>

In der Datei src/test/resources/log4j.xml wird eingestellt, wie viele Details man von den einzelnen Packages oder Klassen sehen will. Wobei <root> die Details für alle sonstigen Klassen bestimmt:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration debug="true" xmlns:log4j='http://jakarta.apache.org/log4j/'>

    <appender name="console" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" 
                value="%d{yyyy-MM-dd HH:mm:ss} %-5p %-40c %m%n"/>
        </layout>
    </appender>
    
    <category name="org.osjava">
        <level value="TRACE"/>        
    </category>

    <category name="org.hibernate">
        <level value="INFO"/>        
    </category>
    
    <category name="org.hibernate.SQL">
        <level value="DEBUG"/>        
    </category>

    <root>
        <level value="INFO"/>
        <appender-ref ref="console"/>
    </root>

</log4j:configuration>

Beim Wildfly Server wird das Logging hingegen in der Datei wildfly-12.0.0.Final/standalone/​configuration/standalone.xml konfiguriert.

Nachwort

Seien Sie sich bewusst, dass die JUnit Tests nicht von einem Applikations-Server ausgeführt werden. Die Java Transaction API (JTA) und weitere Server-spezifische Funktionen stehen daher nicht zur Verfügung.

Simple-JNDI stellt eine Datenbank-Verbindung über JNDI zur Verfügung, so dass man die persistence.xml ohne spezielle Anpassung weiter verwenden kann.

Apache Deltaspike stellt einen CDI Container bereit, mit dem man große Teile von Enterprise Anwendungen ausführen und mocken kann.

Damit eine Anwendung mit JUnit testbar ist, muss sie so gestaltet sein, dass einzelne Teile durch Mocks ersetzbar sind. Das gilt ganz besonders für externe Kommunikations-Schnittstellen. Intensive Nutzung von Dependency Injection mit CDI ist ein guter Schritt in diese Richtung.

Empfehlungen

Wer seine JUnit Tests in einem vollwertigen Applikation-Server durchführen muss, sollte sich Arquillian anschauen.

In den obigen Beispielen haben wir Mocks von der HttpInterface Klasse erzeugt. Mit dem EasyMock Framework kann man dies auf etwas weniger Code-Zeilen reduzieren. Zudem stellt EasyMock einige Zusatzfunktionen (wie Aufruf-Zähler) bereit, die eventuell hilfreich sind. In diesem Zusammenhang ist auch Mockito recht beliebt.

Zur Vorbereitung und Kontrolle von Datenbanken sind die Erweiterungen DbUnit und Arquillian Persistence Extension hilfreich.