1. Einleitung

Im Java EE Kontext wird zur Ausführung von Integrationstests gerne das Arquillian Testframework eingesetzt. Es ermöglicht das Testen im CDI-/EJB-Container und somit unter nahezu realen Bedingungen.

Lange Zeit war die Kombination von Arquillian mit JUnit 5 jedoch nicht möglich. Stand jetzt (Februar 2021) sind die wesentlichen Hürden aus dem Weg geräumt worden. Die neue Version von Arquillian mit JUnit 5 Unterstützung befindet sich zwar noch in einer frühen Alpha-Phase, aber sie ist bereits nutzbar. Ich habe ein Beispielprojekt angelegt und bisher keine Probleme festgestellt.

Den vollständigen Code des lauffähigen Beispielprojektes (Java EE 8) findest du auf GitHub: https://github.com/MatthiasPischka/java-ee-8-beispiele/tree/master/arquillian-junit5-wildfly

2. Vorbereitung

Im Fokus soll der Einsatz von Arquillian mit JUnit 5 stehen. Deshalb gehe ich nicht auf Details zu den üblichen Konfigurationsdateien einer Java EE 8 Anwendung mit CDI- und Web-Support ein (beans.xml, web.xml). Diese sind im GitHub-Repository zu finden.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  ...
  <artifactId>arquillian-junit5-wildfly</artifactId>
  <packaging>war</packaging>

  <properties>
    <junit.version>5.7.0</junit.version>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.jboss.arquillian</groupId>
        <artifactId>arquillian-bom</artifactId>
        <version>1.7.0.Alpha6</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!-- wildfly-arquillian-container-remote bringt zurzeit eine Version von picketbox mit, die nicht im Maven
      Central veröffentlicht wurde. Deshalb wird hier eine andere Version vorgegeben. -->
      <dependency>
        <groupId>org.picketbox</groupId>
        <artifactId>picketbox</artifactId>
        <version>5.1.0.Final</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>8.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.wildfly.arquillian</groupId>
      <artifactId>wildfly-arquillian-container-remote</artifactId>
      <version>3.0.1.Final</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.jboss.arquillian.junit5</groupId>
      <artifactId>arquillian-junit5-container</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.jboss.shrinkwrap.resolver</groupId>
      <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.3.0</version>
      </plugin>
    </plugins>
  </build>
</project>

Die pom.xml sorgt u.a. dafür die benötigten Abhängigkeiten heranzuziehen. Die Angabe von finalName definiert den Namen der war-Datei. Das ist hier besonders wichtig, da ich diesen Namen in Arquillian weiterverwende (siehe unten).

  • junit.version: Hierdurch wird gesteuert, dass JUnit 5 verwendet werden soll.
  • arquillian-bom: Ab der Version 1.7.0.Alpha5 ist die Ausführung von JUnit 5 Tests möglich. Hier wird die aktuellste (Stand Februar 2021) verwendet.
  • wildfly-arquillian-container-remote: Adapter zur Kommunikation mit dem WildFly Application Server 21.x.
  • arquillian-junit5-container: Verbindung zwischen JUnit 5 und Arquillian.
  • shrinkwrap-resolver-impl-maven: Zur Erzeugung eines Testarchivs.

3. Implementierung

Anwendung

Zur Veranschaulichung habe ich ein REST-Endpoint geschrieben, der eine CDI-Bean zur Ausführung verwendet. Diese CDI-Bean wird später im JUnit-Test injiziert, um die Lauffähigkeit von Arquillian zu demonstrieren.

REST-Konfiguration

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/")
public class HalloApplication extends Application {
}

REST-Endpoint

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/hallo")
public class HalloRessource {

  @Inject
  private HalloBean halloBean;

  @GET
  public String hallo() {
    return this.halloBean.sagHalloZu("Arquillian mit JUnit5");
  }
}

CDI-Bean

import javax.enterprise.context.RequestScoped;

@RequestScoped
public class HalloBean {

  public String sagHalloZu(String name) {
    return "Hallo " + name;
  }
}

Das Injizieren der gezeigten CDI-Bean in den REST-Endpoint funktioniert natürlich nur in einem CDI-Container. Wird die Anwendung beispielsweise in einen lokalen WildFly Application Server ausgeführt, ist der REST-Endpoint unter folgender URL zu erreichen: http://localhost:8080/arquillian-junit5-wildfly/hallo

Wie du die Beispiel-Anwendung ganz einfach ausführen kannst, erfährst du weiter unten.

Tests

Ich habe zwei JUnit-Tests angelegt, um den Unterschied zwischen der normalen und der Ausführung über Arquillian aufzuzeigen.

Normale Ausführung

package de.pischka.arquillian.junit5;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;
import java.util.logging.Logger;

/**
 * Diese Testklasse läuft außerhalb von Arquillian. Hier wird erwartet, dass die per CDI injizierte Bean NULL ist.
 */
public class HalloTest {

  private static final Logger LOGGER = Logger.getLogger(HalloTest.class.getName());

  @Inject
  private HalloBean halloBean;

  @Test
  public void testeOhneCdi() {
    LOGGER.info("Test-Ausführung außerhalb des Containers");
    Assertions.assertThrows(NullPointerException.class, () -> this.halloBean.sagHalloZu("JUnit ohne Arquillian"));
  }
}

Bei diesem Test wird versucht die CDI-Bean (HalloBean) zu injizieren. Das sollte bei einer normalen Ausführung des JUnit-Tests allerdings nicht funktionieren. Deshalb rechne ich hier mit einer NullPointerException.

Ausführung über Arquillian

package de.pischka.arquillian.junit5.container;


import de.pischka.arquillian.junit5.HalloBean;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.importer.ZipImporter;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import javax.inject.Inject;
import java.io.File;
import java.util.Objects;
import java.util.logging.Logger;

/**
 * Diese Testklasse läuft mit Arquillian im Container. Hier wird erwartet, dass die per CDI injizierte Bean vorhanden
 * ist.
 */
@ExtendWith(ArquillianExtension.class)
public class HalloTestImContainer {

  private static final Logger LOGGER = Logger.getLogger(HalloTestImContainer.class.getName());

  @Inject
  private HalloBean halloBean;

  @Test
  public void testeMitCdi() {
    LOGGER.info("Test-Ausführung im Container");
    System.out.println("##### Dieser Text erscheint im Server-Log #####");
    Assertions.assertEquals("Hallo Arquillian", this.halloBean.sagHalloZu("Arquillian"));
  }

  /**
   * Erzeugt das Deployment zur Ausführung des Tests.
   *
   * @return Deployment Archiv
   */
  @Deployment
  public static WebArchive createDeployment() {
    LOGGER.info("Erzeuge Deployment-Archiv für die Test-Ausführung");

    ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    File classpathRoot = new File(Objects.requireNonNull(classLoader.getResource("")).getPath());

    /* Verwende das Archiv des normalen Deployments als Grundlage */
    String pfadZurWar = classpathRoot.getParent().concat("/arquillian-junit5-wildfly.war");

    WebArchive archive = ShrinkWrap.create(ZipImporter.class, "test-arquillian-junit5-wildfly.war")
        .importFrom(new File(pfadZurWar))
        .as(WebArchive.class);

    /* Füge zu testende Klassen hinzu */
    archive.addPackages(true, "de.pischka.arquillian.junit5.container");

    File[] libs = Maven.resolver()
        .loadPomFromFile("pom.xml")
        .importTestDependencies()
        .resolve().withTransitivity().asFile();

    /* Füge alle Test-Dependencies aus der pom.xml hinzu */
    for (File file : libs) {
      archive.addAsLibrary(file);
    }

    return archive;
  }
}

Im Grunde genommen ist das der gleiche Test. Auch hier wird versucht die HalloBean zu injizieren. Allerdings wird die Test-Ausführung über @ExtendWith(ArquillianExtension.class) an Arquillian delegiert.

Arquillian benötigt ein Archiv, welches es im Application Server VOR Ausführung der Tests installiert und NACH Ausführung wieder entfernt. Dazu ist eine Methode zu schreiben, die mit @Deployment annotiert wird und ein derartiges Archiv zurück liefert. Was passiert hier im Detail?

  • Das normale war-Archiv wird als Grundlage verwendet (ShrinkWrap.create). Das mache ich so, da dies bereits die lauffähige Anwendung ist. Was dem Archiv noch fehlt sind die Test-Klassen, -Ressourcen und -Abhängigkeiten.
  • Anschließend werden alle zu testenden Klassen hinzugefügt. Das kann u.a. (wie in diesem Beispiel) über die Angabe von Packages erfolgen. Auch Wildcards wie de.pischka.* sind möglich.
  • Als letztes werden über die pom.xml alle Test-Abhängigkeiten aufgelöst und ebenfalls dem Archiv hinzugefügt.

Die gezeigten JUnit-Tests können auf üblichem Weg ausgeführt werden. Schaltet sich Arquillian ein, so wird per Standard der lokal installierte WildFly Application Server angesprochen. Sollte es sich um einen entfernten Server oder beispielsweise ein Server in einem Docker Container (siehe auch weiter unten) handeln, benötigt Arquillian weitere Angaben. Dazu ist dann eine arquillian.xml im Verzeichnis src/test/resources anzulegen:

<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns="http://jboss.org/schema/arquillian"
            xsi:schemaLocation="http://jboss.org/schema/arquillian
    http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

  <container qualifier="wildfly_remote_arquillian" default="true">
    <configuration>
      <property name="managementAddress">127.0.0.1</property>
      <property name="managementPort">9990</property>
      <property name="username">admin</property>
      <property name="password">admin1234</property>
      <property name="connectionTimeout">10000</property>
    </configuration>
  </container>
</arquillian>

Sie sollte die Adresse zum Management-Zugang des WildFly enthalten. In dem gezeigten Beispiel habe ich das connectionTimeout auf 10 Sekunden erhöht (Standard: 5 Sekunden), da ein im Docker-Container laufender WildFly nicht immer auf Anhieb reagiert.

Das ist alles, um Tests mit Arquillian und JUnit 5 in einem WildFly Application Server 21.x auszuführen.

4. Ausführen mit Docker

Das vollständige Beispiel auf GitHub enthält einen Ordner Docker. Dieser enthält das benötigte Dockerfile sowie Shell-Skripte zum Bauen der Anwendung per Maven, Bauen des Images inklusive war-File per Docker und Starten des Docker-Containers. Sobald der Server im Container läuft, können die JUnit-Tests ausgeführt werden. Der zuletzt gezeigte Test sollte dann im Docker-Container laufen.

Eine genaue Anleitung ist im Hauptverzeichnis der Java EE Beispiele zu finden: https://github.com/MatthiasPischka/java-ee-8-beispiele

Update (11.07.2021)

Gut ein halbes Jahr nach der Veröffentlichung des Artikels befindest sich die Version von Arquillian, die JUnit 5 Tests ermöglicht, weiterhin im Alpha-Stadium. Das Team hinter Arquillian lässt sich mit Version 1.7.0 viel Zeit. Allerdings wird weiter daran gearbeitet, sodass ich das Beispiel auf GitHub mit den neusten Versionen von JUnit 5 (jetzt 5.7.2) und Arquillian 1.7.0.Alpha (jetzt Alpha10) aktualisiert habe.

Die Stellen, die geändert wurden, sind im Commit ersichtlich: https://github.com/MatthiasPischka/java-ee-8-beispiele/commit/e530e8013006f28580f8704e00390fdf1e456305


0 Kommentare

Schreibe einen Kommentar

Avatar-Platzhalter

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert