Beispiel: Unit-Test mit Maven und Arquillian


Inhalt:

Arquillian
Unittest oder Integrationstest?
Code des Tests
Erzeugen des deploybaren Artefakts über Shrinkwrap
Konfiguration des Tests: Profiles
Konfiguration des Tests: Profile für "Remote Server"
Konfiguration des Tests: Profile für "Managed Server"
Variante 1: Ausführen als Maven-Test über Eclipse
Variante 2: Ausführen außerhalb Eclipse über Maven
Variante 3: Ausführen als Eclipse-Unit-Test
Debugging der Unit Test-Clientseite
Ausführen mit Java 17
Wie der Unit-Test abläuft


Für WildFly 26: hier wird ein Unit-Test für ein EAR-Projekt, bestehend aus EJB-Projekt und Web-Projekt, unter Verwendung des Arquillian-Framework, mittels Maven erstellt und deployed.

Dieses Beispiel basiert komplett auf dem Beispiel Stateless Session Bean und Maven (dort finden sich Erklärungen zur Struktur und zu den diversen "pom.xml") und erweitert es nur um einen Unit-Test. Dieser Test ist ein Integrationstest, da er eine EJB testet, die in der Web-Schicht der Anwendung verwendet wird.

Hier gibt es das gepackte Eclipse-Projekt zum Download: StatelessMaven.zip. Die Importanleitung findet man ebenfalls im Stateless Session Bean und Maven-Beispiel.


Arquillian

Das Arquillian-Framework (http://arquillian.org/) ist ein Aufsatz auf die bekanntesten Unittestframeworks, darunter JUnit oder auch TestNG. Es erweitert diese Frameworks so, dass Unit-Tests für Server-Anwendungen durchgeführt werden können: der zu testende Code läuft auf einem JavaEE-Server, während der Unit-Test-Client in seiner eigenen JVM, eventuell sogar auf einem anderen Rechner, läuft und die Aufrufe der Testmethoden vom Client zum Server "getunnelt" werden. Mehr dazu weiter unten.


Unittest oder Integrationstest?

Maven hat zwei Plugins für Tests: für Unittests wird der "maven-surefire-plugin" verwendet, für Integrationstests der "maven-failsafe-plugin".

Für uns relevant ist die Position der Testtypen im "maven build life cycle" (Quelle:
http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html):
  1. validate
  2. initialize
  3. generate-sources
  4. process-sources
  5. generate-resources
  6. process-resources
  7. compile
  8. process-classes
  9. generate-test-sources
  10. process-test-sources
  11. generate-test-resources
  12. process-test-resources
  13. test-compile
  14. process-test-classes
  15. test
  16. prepare-package
  17. package
  18. pre-integration-test
  19. integration-test
  20. post-integration-test
  21. verify
  22. install
  23. deploy
Der "maven-surefire-plugin" wird in Phase 15 ("test") durchgeführt, der "maven-failsafe-plugin" in Phase 19 ("integration-test"). Dazwischen erfolgt die Phase "package", und diese ist für uns wichtig: wir benötigen weiter unten im Schritt Erzeugen des deploybaren Artefakts über Shrinkwrap die WAR-Datei, die in der Phase "package" vom "maven-war-plugin" erzeugt wurde. Und diese WAR-Datei ist noch nicht da, wenn der Schritt "test" erfolgt. Das heißt wir können unseren Test nicht als Unit-Test implementieren ("maven-surefire-plugin"), sondern müssen ihn als Integrationstest implementieren ("maven-failsafe-plugin").

Wir müssen die Testklassen entsprechend benennen, damit sie vom jeweiligen Maven-Plugin verarbeitet werden:
Das bedeutet: unsere Testklasse muss GeometricModelBeanIT heißen!

Code des Tests

In Maven-Projekten liegt Unittest-Code in einem eigenen Unterverzeichnis "src\test". In diesem Verzeichnis fügen wir die Klasse de.hsrm.cs.javaee8.statelessmaven.web.test.GeometricModelBeanIT zu:
Testklasse

Sie hat diesen Code:
package de.hsrm.cs.javaee8.statelessmaven.web.test;

import javax.ejb.EJB;

import org.jboss.arquillian.junit.Arquillian;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import de.hsrm.cs.javaee8.statelessmaven.ejb.GeometricModelLocal;

@RunWith(Arquillian.class)
public class GeometricModelBeanIT
{
  @EJB
  private GeometricModelLocal geometricModelLocal;
  
  @Test
  public void testCuboidSurface()
  {
    System.out.println("Testing cuboid surface...");
    
    double surface = geometricModelLocal.computeCuboidSurface(1, 2, 3);
    //Hier muss ein "Delta" angegeben werden für Abweichung durch Rundungsungenauigkeit. Das kann hier nicht zuschlagen, deshalb können wir Delta = 0 angeben.
    Assert.assertEquals(22, surface, 0);
  }
  
  @Test
  public void testCuboidVolume()
  {
    System.out.println("Testing cuboid volume...");
                
    double volume = geometricModelLocal.computeCuboidVolume(1, 2, 3);
    //Hier muss ein "Delta" angegeben werden für Abweichung durch Rundungsungenauigkeit. Das kann hier nicht zuschlagen, deshalb können wir Delta = 0 angeben.
    Assert.assertEquals(6, volume, 0);
  }
}
Das Local Interface der EJB wird vom Server injiziert. Die Test-Methoden prüfen die Berechnung von Oberfläche und Volumen.
Die Annotation org.junit.runner.RunWith hat als Parameter die Klasse org.jboss.arquillian.junit.Arquillian: dies sorgt dafür, dass für das Ausführen des Tests das Arquillian-Framework verwendet wird.

Fürs Compilieren des Code müssen in "pom.xml" von "StatelessMaven-web" folgende Dependencies enthalten sein (dies ist durch die Erzeugung des Projekts aus dem Archetype "wildfly-jakartaee-ear-archetype" schon gegeben):

	<dependencies>
		...	
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.jboss.arquillian.junit</groupId>
			<artifactId>arquillian-junit-container</artifactId>
			<scope>test</scope>
		</dependency>
		...
	</dependencies>
Falls man selbst Dependencies hinzufügt, dann muss danach im Contextmenü "Maven" => "Update Project" aufgerufen werden, damit Eclipse diese sieht.

Die Versionen werden hier nicht angegeben, sie ergeben sich aus dem WildFly-BOM, in unserem Fall aus
https://repo.maven.apache.org/maven2/org/wildfly/bom/wildfly-jakartaee8-with-tools/26.0.0.Final/wildfly-jakartaee8-with-tools-26.0.0.Final.pom, das in "pom.xml" des Root-Projekts eingebunden ist. Dies sind JUnit 4.13.1 und Arquillian 1.6.0.Final.

Der Wert "test" des Elements "scope" bedeutet, dass diese Abhängigkeit nur zur Compilierung und Ausführung von Testklassen zur Verfügung steht, aber nicht im regulären Code der Anwendung.


Erzeugen des deploybaren Artefakts über Shrinkwrap

Wie eingangs beschrieben führt Arquillian den Testcode auf dem Server aus. Damit dies klappt, muss (je nach Anwendungsfall) eine EJB-JAR-Datei, eine WAR-Datei oder eine EAR-Datei erzeugt werden, die den Testcode enthält. Maven erzeugt zwar beim Ausführen der Anwendung (Goal "wildfly:deploy") ebenfalls eine EAR-Datei. Diese enthält aber nicht die Testklassen. Das Arquillian-Framework bietet eine API namens "ShrinkWrap" (http://arquillian.org/modules/shrinkwrap-shrinkwrap/), die für das Erzeugen von deploybaren Artefakten zuständig ist: sie kann JAR-, WAR- und EAR-Dateien erzeugen. Sie ist enthalten in der im letzten Schritt eingebundenen Dependency mit der GroupID "org.jboss.arquillian.junit" und der ArtifactId "arquillian-junit-container".

ShrinkWrap-Einführung: http://arquillian.org/guides/shrinkwrap_introduction/.

Die Unit-Test-Klasse erhält eine Methode, die ein Generic org.jboss.shrinkwrap.api.Archive zurückliefert und mit der Annotation org.jboss.arquillian.container.test.api.Deployment versehen ist:
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.Archive;

@RunWith(Arquillian.class)
public class GeometricModelBeanIT
{
  @Deployment
  public static Archive<?> getEarArchive()
  {
  }
}

Da für den Maven-Testlauf ein Compile und Build durchgeführt wird, verwenden wir soweit möglich die vom Build-Prozess erzeugten Artefakte und modifizieren diese nur noch.
Der Ablauf beim Maven-Testlauf ist, dass zuerst das "innerste" Projekt compiliert wird und dessen Tests (sofern vorhanden) durchgeführt werden. Danach sind die Projekte an der Reihe, die dieses Projekt als Abhängigkeit haben, und am Schluss ist das Root-Projekt an der Reihe. In unserem Fall hat das Web-Projekt eine Abhängigkeit zum EJB-Projekt, und das EAR-Projekt hat EJB- und Web-Projekt als Abhängigkeiten.
Der Ablauf ist also dieser: Da unsere Integrationstests im Web-Projekt liegen, sind zum Zeitpunkt der Ausführung der Tests das EJB-Projekt und das Webprojekt compiliert und die JAR- bzw. WAR-Dateien erzeugt. Allerdings ist die EAR-Datei für das Gesamtprojekt noch nicht erzeugt!
Wir können für das Erzeugen des Deployment also EJB-JAR und Web-WAR verwenden, und müssen diese zur EAR-Datei zusammenbündeln. Der Code sieht so aus:
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.importer.ZipImporter;
import org.jboss.shrinkwrap.api.spec.EnterpriseArchive;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
...

@RunWith(Arquillian.class)
public class GeometricModelBeanIT
{
  @Deployment
  public static Archive<?> getEarArchive() 
  {
    EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class, "StatelessMaven-ear.ear");
    
    File f = new File("../StatelessMaven-ejb/target/StatelessMaven-ejb.jar");
    JavaArchive ejbJar = ShrinkWrap.create(ZipImporter.class, "StatelessMaven-ejb.jar").
          importFrom(f).as(JavaArchive.class) ;
    ear.addAsModule(ejbJar);
     
    
    f = new File("../StatelessMaven-web/target/StatelessMaven-web.war");
    WebArchive war = ShrinkWrap.create(ZipImporter.class, "StatelessMaven-web.war").
           importFrom(f).as(WebArchive.class) ;
    ear.addAsModule(war);
    
    //hier kommt weiter unten noch "application.xml"...
    
    war.addPackage("de.hsrm.cs.javaee8.statelessmaven.web.test");
      
    return ear;
  }
}
Zur Erklärung: Es fehlt noch die Datei "application.xml", siehe unten. Und es gibt einen weiteren Unterschied zu der EAR-Datei, die Maven beim Build erzeugt: die "Manifest.mf" fehlt uns. Aber diese ist nicht benötigt.


Falls wir uns anschauen wollen, was die ShrinkWrap-API für eine Datei erzeugt, können wir diese exportieren lassen:

import org.jboss.shrinkwrap.api.exporter.ZipExporter;
...

    ear.as(ZipExporter.class).exportTo(new File("c:\\temp\\test.ear"), true);

Wenn der Maven-Build-Ablauf so wäre, dass zuerst alle Projekt durchcompiliert und die Archivdateien erzeugt werden, dann könnte man die vom EAR-Projekt-Build erzeugte EAR-Datei verwenden und diese erweitern. Das würde den Code deutlich vereinfachen:

    EnterpriseArchive ear = ShrinkWrap.create(ZipImporter.class, "StatelessMaven-ear.ear")
        .importFrom(new File("../StatelessMaven-ear/target/StatelessMaven-ear.ear")).as(EnterpriseArchive.class);
    WebArchive war = ear.getAsType(WebArchive.class, "/StatelessMaven-web.war");
	war.addPackage("de.hsrm.cs.javaee8.statelessmaven.web.test");
      
    return ear;
Allerdings funktioniert dies in der Realität nicht, weil diese EAR-Datei vermutlich noch nicht vorhanden ist. Sie ist dann da, wenn vor dem aktuellen Maven-Testlauf das Goal "install" gestartet hat, dass das gesamte Projekt compiliert. Dieses Goal führt zwar Unit-Tests durch, aber da wir den "maven-failsafe-plugin" verwenden und dieser nur in zwei Profilen aktiv wird, in der Default-Konfiguration aber nicht aktiv ist, werden unsere Integrationstests nicht durchgeführt.

Dieses händische Aufrufen von "install" vor den Integrationstests ist allerdings keine praxistauglichen Lösungen, d.h. man muss damit leben dass Maven zwar die perfekte EAR-Datei erzeugen würde, man sie aber trotzdem über die ShrinkWrap-API nachbauen muss.

Erzeugen von "application.xml"
Der Maven-Buildprozess des EAR-Projekts erzeugt diese "application.xml" (zu finden in "StatelessMaven-ear\target"):
<?xml version="1.0" encoding="UTF-8"?>
<application 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/application_8.xsd" version="8">
  <display-name>StatelessMaven-ear</display-name>
  <module>
    <web>
      <web-uri>StatelessMaven-web.war</web-uri>
      <context-root>/StatelessMaven-web</context-root>
    </web>
  </module>
  <module>
    <ejb>StatelessMaven-ejb.jar</ejb>
  </module>
  <library-directory>lib</library-directory>
</application>
Diese wollen wir beim Erzeugen des Deployments ebenfalls erzeugen. Leider können wir nicht die vorhandene Datei recyclen, weil je nach Maven-Ablauf nicht sichergestellt ist, dass sie da ist.
Die einfachste Lösung wäre es, eine solche Datei irgendwo im Projekt abzulegen und sie einzulesen. Aber wir gehen hier den aufwändigen Web über die ShrinkWrap-API bzw. deren Unterprojekt "ShrinkWrap Descriptors" (http://arquillian.org/modules/descriptors-shrinkwrap/).

Bei WildFly 26 wird Arquillian 1.6.0.Final verwendet. Dort ist "shrinkwrap-descriptors" bereits eingebunden, siehe https://repo.maven.apache.org/maven2/org/jboss/arquillian/arquillian-bom/1.6.0.Final/arquillian-bom-1.6.0.Final.pom. Deshalb steht können wir die Artefakte dieser BOM automatisch verwenden.


In "arquillian-bom-1.6.0.Final.pom" ist dies so eingetragen (allerdings ist die Versionsnummer dort nur als Property eingetragen, die weiter oben in der Datei mit dem Wert "2.0.0" definiert ist):
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.jboss.shrinkwrap.descriptors</groupId>
        <artifactId>shrinkwrap-descriptors-bom</artifactId>
        <version>2.0.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
Diese Dependency stellt uns nur einen Satz von Dependencies zur Verfügung, aus denen das ShrinkWrap Descriptors-Projekt besteht. Im eigentlichen "dependencies"-Element können wir auf diese zugreifen. Der "scope"-Wert "import" steht nur im Element "dependencyManagement" zur Verfügung. Es bedeutet dass die aktuelle Dependency durch die im Element "dependencyManagement" definierten Dependencies des importierten Artefakts ersetzt wird. Die so bereitgestellten Dependencies stehen aber noch nicht in Quellcode oder zur Ausführung zur Verfügung, sondern müssen explizit aktiviert werden, siehe nächster Schritt.

Im Element "project" => "dependencies" fügen wir jetzt die eigentliche Abhängigkeit hinzu:

	<dependencies>
		...
		<dependency>
			<groupId>org.jboss.shrinkwrap.descriptors</groupId>
			<artifactId>shrinkwrap-descriptors-depchain</artifactId>
			<type>pom</type>
			<scope>test</scope>
		</dependency>
	</dependencies>
Die Version dieser Dependency ergibt sich aus der im vorherigen Schritt definierten "shrinkwrap-descriptors-bom". Dies ist wiederum nur ein Sammel-Artifact, das eine Reihe von JAR-Dateien einbindet, für die verschiedenen Typen von Deployment-Deskriptoren, darunter die JavaEE-Standard-Deskriptoren, aber auch die WildFly-spezifischen wie "jboss-web.xml".


Jetzt können wir den Code unserer "getEarArchive"-Methode erweitern, so dass "application.xml" erzeugt wird:
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.descriptor.api.Descriptors;
import org.jboss.shrinkwrap.descriptor.api.application7.ApplicationDescriptor;

@RunWith(Arquillian.class)
public class GeometricModelBeanIT
{
  @Deployment
  public static Archive<?> getEarArchive() 
  {
    ...
    ear.addAsManifestResource(f, "StatelessMaven-ds.xml");
    
    ApplicationDescriptor descriptor = Descriptors.create(ApplicationDescriptor.class);

    descriptor.applicationName("StatelessMaven-ear");
    //Webmodul bauen:
    descriptor.createModule().getOrCreateWeb().contextRoot("/StatelessMaven-web").webUri("StatelessMaven-web.war");
    
    //EJB-Modul reinpacken:
    descriptor.createModule().ejb("StatelessMaven-ejb.jar");
    
    //In EAR-Datei schreiben:
    ear.setApplicationXML(new StringAsset(descriptor.exportAsString()));
	
    war.addPackage("de.hsrm.cs.javaee8.statelessmaven.web.test");
      
    return ear;
  }
}
Wie man an dem Import org.jboss.shrinkwrap.descriptor.api.application7.ApplicationDescriptor sieht, kann die API keine JavaEE8-Deskriptoren erzeugen. Es sieht auch nicht so aus, als würde sie aktiv weiterentwickelt...

Falls wir eine vorhandene "application.xml" irgendwo abgelegt hätten, könnten wir diese so ins EnterpriseArchive schreiben:
@RunWith(Arquillian.class)
public class GeometricModelBeanIT
{
  @Deployment
  public static Archive<?> getEarArchive() 
  {
    ...
    f = new File("../pfad/zu/application.xml");
    ear.setApplicationXML(f);
	
    ear.addAsManifestResource(f, "StatelessMaven-ds.xml");
    ...
  }

Eigentlich ist die "application.xml" garnicht nötig, man könnte sich den Aufwand also sparen ;-)


Konfiguration des Tests: Profiles

Das Arquillian-Framework übernimmt hier die Testdurchführung. Im Rahmen des Testlaufs wird ein laufender WildFly-Server benötigt. Es gibt zwei Varianten:
In beiden Fällen wird das Verfahren durch Einträge in "pom.xml" sowie durch eine zusätzliche Datei "arquillian.xml" gesteuert. Der Archetype bereitet das Projekt schon entsprechend vor, im Folgenden wird deshalb nur der Ist-Zustand beschrieben:

In "pom.xml" des Root-Projekts "StatelessMaven" steht eine Property, die die Version für den "maven-failsafe-plugin" definiert:
        <version.surefire.plugin>2.22.1</version.surefire.plugin>
        <version.failsafe.plugin>2.22.1</version.failsafe.plugin>
        <version.war.plugin>3.2.2</version.war.plugin>

In "pom.xml" des Projekts "StatelessMaven-Web" ist die Abhängigkeit "arquillian-protocol-servlet" eingetragen:


		<dependency>
			<groupId>org.jboss.arquillian.protocol</groupId>
			<artifactId>arquillian-protocol-servlet</artifactId>
			<scope>test</scope>
		</dependency>

Die Datei "arquillian.xml" im Projekt "StatelessMaven-web" im Unterverzeichnis "src\test\resources" wird benötigt:
arquillian.xml
Sie hat diesen Inhalt:
<arquillian xmlns="http://jboss.org/schema/arquillian"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
        http://jboss.org/schema/arquillian
        http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

	<!-- Sets the protocol which is how Arquillian talks and executes the tests inside the container -->
	<defaultProtocol type="Servlet 3.0" />

	...
</arquillian>
Das Element "defaultProtocol" gibt an, mit welchem Verfahren Arquillian arbeiten soll und steht hier auf "Servlet 3.0" - siehe Abschnitt
Wie der Unit-Test abläuft.

An die Stelle der drei Punkte kommen die Konfigurationen für die Remote- bzw. Managed Server.

Man könnte sich für eines der beiden Verfahren entscheiden, oder man legt sich Maven-Profile an und gibt beim Testlauf an, welches Profil zu verwenden ist. Im Folgenden wird die Variante mit zwei Profilen beschrieben.

ACHTUNG:
In "arquillian.xml" dürfen keine Umlaute (oder allgemein: Zeichen mit ASCII-Index höher als 127) verwendet werden. Ansonsten erhält man diesen Fehler:
[INFO] Running de.hsrm.cs.javaee7.statelessmaven.web.test.GeometricModelBeanIT
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.264 s <<< FAILURE! - in de.hsrm.cs.javaee7.statelessmaven.web.test.GeometricModelBeanIT
[ERROR] de.hsrm.cs.javaee7.statelessmaven.web.test.GeometricModelBeanIT  Time elapsed: 0.263 s  <<< ERROR!
java.lang.RuntimeException: Could not create new instance of class org.jboss.arquillian.test.impl.EventTestRunnerAdaptor
Caused by: java.lang.reflect.InvocationTargetException
Caused by: org.jboss.shrinkwrap.descriptor.api.DescriptorImportException: Could not import XML from stream
Caused by: com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Ungültiges Byte 1 von 1-Byte-UTF-8-Sequenz.
Hier liegt wohl ein Bug im Parser der Datei vor: https://issues.jboss.org/browse/SHRINKDESC-97. In meinem Fall ist die XML-Datei sowieso unsauber definiert, da sie selbst kein Encoding definiert, und das Default-Encoding des Eclipse-Projekts auf "UTF-8" steht. Aber sogar wenn in "arquillian.xml" ein sauberes Encoding definiert wird, bleibt der Fehler erhalten.

Konfiguration des Tests: Profile für "Remote Server"

In "pom.xml" des Root-Projekts "StatelessMaven" gibt es Root-Element "project" ein Unterelement "profiles", darin ist ein Profile mit dem Namen "arq-remote" definiert:
	<profiles>
		<!-- Arquillian WildFly remote profile -->
		<profile>
			<id>arq-remote</id>
			<activation>
				<activeByDefault>false</activeByDefault>
			</activation>
			<dependencies>
				<dependency>
					<groupId>org.wildfly.arquillian</groupId>
					<artifactId>wildfly-arquillian-container-remote</artifactId>
					<scope>test</scope>
				</dependency>
			</dependencies>

			<build>
				<plugins>
					<plugin>
						<artifactId>maven-failsafe-plugin</artifactId>
						<version>${version.failsafe.plugin}</version>
						<executions>
							<execution>
								<goals>
									<goal>integration-test</goal>
									<goal>verify</goal>
								</goals>
							</execution>
						</executions>
						<configuration>
							<!-- Variablen für "arquillian.xml" definieren: -->
							<systemPropertyVariables>
								<!-- Defines the container qualifier in "arquillian.xml" -->
								<arquillian.launch>remote</arquillian.launch>
							</systemPropertyVariables>
						</configuration>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>
Die wichtigsten Einstellungen in diesem Element:

In "arquillian.xml" wird ein "container" eingetragen:

	<!-- Configuration to be used when the WildFly remote profile is active -->
	<container qualifier="remote">
		<configuration>
			<property name="managementAddress">127.0.0.1</property>
			<property name="managementPort">9990</property>
			<!-- If deploying to a remote server, you have to specify username/password here -->
			<!--
			<property name="username">admin</property>
			<property name="password">admin</property>
			-->
		</configuration>
	</container>


Konfiguration des Tests: Profile für "Managed Server"

In "pom.xml" legen wir uns im Root-Element "project" ein Unterelement "profiles" an, darin wird ein Profile mit dem Namen "arq-managed" definiert:
    <profiles>
        ...
        
        <profile>
            <!-- An optional Arquillian testing profile that executes tests in your JBoss EAP instance.
                 This profile will start a new JBoss EAP instance, and execute the test, shutting it down when done.
                 Run with: mvn clean verify -Parq-managed -->
            <id>arq-managed</id>
            <dependencies>
                <dependency>
                    <groupId>org.wildfly.arquillian</groupId>
                    <artifactId>wildfly-arquillian-container-managed</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-failsafe-plugin</artifactId>
                        <version>${version.failsafe.plugin}</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>integration-test</goal>
                                    <goal>verify</goal>
                                </goals>
                            </execution>
                        </executions>
                        <configuration>
                            <!-- Configuration for Arquillian: -->
                            <systemPropertyVariables>
                                <!-- Defines the container qualifier in "arquillian.xml" -->
                                <arquillian.launch>managed</arquillian.launch>
                            </systemPropertyVariables>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        ...
    </profiles>
Die wichtigsten Einstellungen in diesem Element:

In "arquillian.xml" wird ein "container" eingetragen:
    <!-- Configuration to be used when the WildFly managed profile is active -->
    <container qualifier="managed">
        <!-- By default, Arquillian will use the JBOSS_HOME environment variable to find the JBoss EAP installation.
             If you prefer not to define the JBOSS_HOME environment variable, alternatively you can uncomment the
             following `jbossHome` property and replace EAP_HOME with the path to your JBoss EAP installation. -->
        <!--
        <configuration>
            <property name="jbossHome">EAP_HOME</property>
        </configuration>
        -->
    </container>
Alternative: es ist möglich, Maven den Server aus dem Maven Repository herunterladen zu lassen. Danach startet Maven ihn selbst:
	<profiles>
		<!-- Arquillian WildFly managed profile -->
		<profile>
			<id>arq-managed</id>
			<activation>
				<activeByDefault>false</activeByDefault>
			</activation>
			<dependencies>
				<dependency>
					<groupId>org.wildfly.arquillian</groupId>
					<artifactId>wildfly-arquillian-container-managed</artifactId>
					<scope>test</scope>
				</dependency>

			</dependencies>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-dependency-plugin</artifactId>
						<version>3.0.2</version>
						<executions>
							<execution>
								<id>unpack</id>
								<phase>pre-integration-test</phase>
								<goals>
									<goal>unpack</goal>
								</goals>
								<configuration>
									<artifactItems>
										<artifactItem>
											<groupId>org.wildfly</groupId>
											<artifactId>wildfly-dist</artifactId>
											<version>15.0.0.Final</version>
											<type>zip</type>
											<overWrite>false</overWrite>
											<outputDirectory>target</outputDirectory>
										</artifactItem>
									</artifactItems>
								</configuration>
							</execution>
						</executions>
					</plugin>

					<plugin>
						<artifactId>maven-failsafe-plugin</artifactId>
						<version>2.20</version>
						<executions>
							<execution>
								<goals>
									<goal>integration-test</goal>
									<goal>verify</goal>
								</goals>
							</execution>
						</executions>
						<configuration>
							<!-- Variablen für "arquillian.xml" definieren: -->
							<systemPropertyVariables>
								<jboss.home>${project.basedir}/target/wildfly-15.0.0.Final</jboss.home>
								<!-- Auszuführende Konfiguration in "arquillian.xml" -->
								<arquillian.launch>managed</arquillian.launch>
							</systemPropertyVariables>
						</configuration>
					</plugin>
				</plugins>
			</build>
		</profile>
		...
	</profiles>
Unterschiede zur vorherigen Konfiguration:
In "arquillian.xml" sieht der Container "managed" etwas anders aus als im vorherigen Beispiel:

	<!-- Configuration to be used when the WildFly managed profile is active -->
	<container qualifier="managed" default="false" >
		<configuration>
			<property name="jbossHome">${jboss.home}</property>
		</configuration>
	</container>
Bei Ausführung dieser Tests wird der WildFly-Server einmalig aus dem Maven-Repository ins lokale Repository geladen, und beim Testlauf (auch z.B. nach Aufrufen von "clean") neu aus dem lokalen Repository entpackt:
[INFO] --- maven-dependency-plugin:3.0.2:unpack (unpack) @ StatelessMaven-web ---
[INFO] Configured Artifact: org.wildfly:wildfly-dist:11.0.0.Final:zip
[INFO] Unpacking C:\Users\USERNAME\.m2\repository\org\wildfly\wildfly-dist\11.0.0.Final\wildfly-dist-11.0.0.Final.zip to C:\Temp\workspace\StatelessMaven\StatelessMaven-web\target with includes "" and excludes ""
Hier könnte es optimaler sein, wenn man den WildFly-Server in ein externes Verzeichnis legt.


Variante 1: Ausführen als Maven-Test über Eclipse

Weiter unten ist beschrieben, warum der einfache Weg für das Ausführen von Tests aus Eclipse heraus nicht klappt.

Variante 1a: remote Server:
Hier wird das Deploy im Rahmen des Tests gegen einen bereits laufenden WildFly-Server ausgeführt.
Wir legen eine "Run Configuration" in der Gruppe "Maven" an, setzen das "Base directory" auf "${workspace_loc:/StatelessMaven}" und geben bei "Profiles" das auszuführende Profil an: "arq-remote" oder "arq-managed". Da in beiden Profilen der "maven-failsafe-plugin" konfiguriert wurde, stellen wir außerdem das "Goal" "verify" ein (statt "failsafe:integration-test"): das führt dazu dass nach dem Integrationstest geprüft wird dass die Tests erfolgreich waren.
Hier die Konfiguration für das Profile "arq-remote" - der WildFly-Server muss vorher händisch gestartet werden!
Maven configuration (remote server)

Variante 1b: managed Server:
Hier die Konfiguration für das Profile "arq-managed" - hier wird der WildFly-Server von Arquillian gestartet:
Maven configuration (managed server)
Sofern man keine systemweite Umgebungsvariable "JBOSS_HOME" definiert hat, die zum WildFly-Server zeigt, muss man die Umgebungsvariable im Maven-Profil definieren. Dies geschieht auf dem Karteireiter "Environment":
Maven configuration (JBOSS_HOME)

Jetzt können wir den Test laufen lassen.
Bei Verwendung des Profiles "arq-managed" sehen wir die WildFly-Startmeldungen in der gleichen Konsole, in der auch die Ausgabe des Tests liegt.

Eventuell muss auf dem Karteireiter "JRE" ein JDK ausgewählt werden, wenn Eclipse eine JRE verwendet.

Eventuell zeigt der Test eine solche Konsolenausgabe:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-failsafe-plugin:2.20:verify (default) on project StatelessMaven-web: There are test failures.
[ERROR] 
[ERROR] Please refer to C:\Temp\workspace\StatelessMaven\StatelessMaven-web\target\failsafe-reports for the individual test results.
[ERROR] Please refer to dump files (if any exist) [date]-jvmRun[N].dump, [date].dumpstream and [date]-jvmRun[N].dumpstream.
[ERROR] -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
[ERROR] 
[ERROR] After correcting the problems, you can resume the build with the command
[ERROR]   mvn  -rf :StatelessMaven-web
Wie die Fehlermeldung schon besagt, finden wir im Unterverzeichnis "target\failsafe-reports" des Projekts, das den Fehler verursacht hat, eine Logdatei.

Für "reguläre" Unit-Tests würde es reichen, im Contextmenü den Punkt "Run as" => "9 Maven test" aufzurufen.
Maven test

Allerdings funktioniert das nicht, da wir Integrationstests haben. Für deren Ausführung gibt es keinen Contextmenüpunkt. Ein Lösungsansatz wäre: wir öffnen über den Toolbar-Button "Run" die "Run configurations" und legen unter "Maven" eine neue an. Als "Base directory" wählen wir "${workspace_loc:/StatelessMaven}" (Root-Projekt des Multi-Modul-Projekts) aus, als "Goal" geben wir "failsafe:integration-test" ein - das führt alle Schritte des Build Lifecycle bis zum Integrationstest durch.
Maven configuration

Wenn wir als Goal nur "integration-test" angeben, dann scheinen keine Tests ausgeführt zu werden. Grund scheint zu sein, dass der "maven-failsafe-plugin" nicht per Default ausgeführt wird, sondern im "pom.xml" explizit aktiviert werden muss. Das ist hier nicht geschehen. Entsprechend bewirkt auch der Schritt "verify", den man eigentlich bei Aufruf des "maven-failsafe-plugin" verwenden sollte, nichts.

Das Ausführen des Goal "failsafe:integration-test" führt allerdings zu einem Fehler:
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.451 s <<< FAILURE! - in de.hsrm.cs.javaee7.statelessmaven.web.test.GeometricModelBeanIT
[ERROR] de.hsrm.cs.javaee7.statelessmaven.web.test.GeometricModelBeanIT  Time elapsed: 0.45 s  <<< ERROR!
org.jboss.arquillian.container.test.impl.client.deployment.ValidationException: 
DeploymentScenario contains a target (_DEFAULT_) not matching any defined Container in the registry.
Please include at least 1 Deployable Container on your Classpath.
Ursache ist, dass wir zwei Profile angelegt haben und keines davon als Default-Profil definiert ist. Und dies wiederum führt zu den eingangs beschriebenen Wegen für den Start des Unit-Tests.



Variante 2: Ausführen außerhalb Eclipse über Maven

Wir benötigen eine Maven-"Installation" und können dann aus dem "StatelessMaven-web"-Projekt heraus die Unit-Tests unter Angabe eines Profils aufrufen:

c:\pfad\zu\maven\apache-maven-3.5.0\bin\mvn.cmd -Parq-managed verify
Im Beispiel wird das Profil "arq-managed" verwendet.


Variante 3: Ausführen als Eclipse-Unit-Test

Diese Variante hat den Vorteil, dass wir den Unit-Test debuggen können (wobei ein besseres Verfahren im nächsten Schritt beschrieben wird). Allerdings gibt es einige Einschränkungen: Jetzt legen wir eine "Debug Configuration" in der Gruppe "JUnit" an. Es wird die Option "Run all tests in the selected project" gewählt und das Projekt "StatelessMaven-web" angegeben.
Debug configuration
Beim Ausführen dieser Debug Configuration wird das Arquillian-Framework angestoßen, und es sollten jetzt Breakpoints in der mit @Deployment annotierten Methode (diese wird auf Client-Seite ausgeführt) und auch in den @Test-Methoden angesprungen werden.


Debugging der Unit Test-Clientseite

In diesem Abschnitt wird beschrieben, wie die Clientseite der Unit Tests (also die Klasse GeometricModelBeanIT) gedebuggt werden kann. Wird die Run Configuration für den Maven Build im Debugmodus ausgeführt, dann passiert einfach nur nichts - keine Breakpoints werden angesprungen.
Im Web findet sich der Tip, in der Run Configuration einen Parameter "forkCount" auf den Wert "0" zu setzen:
Fork Count

Dies funktioniert in meinem Beispiel allerdings nicht: Es wird es wird eine java.io.FileNotFoundException ausgelöst im Erzeugen des ShrinkWrap-Archivs, und zwar an der Stelle wo "StatelessMaven-ejb.jar" in die EAR-Datei eingebunden werden soll. Ursache ist, dass ein relativer Pfad verwendet wird. Durch die Art und Weise wie Maven die Java VM durch das Setzen des Parameters "forkCount" aufruft, ändert sich für einige Dateizugriffsaufrufe der aktuelle Pfad, siehe auch der Hinweis in
https://bugs.java.com/bugdatabase/view_bug?bug_id=4483097. Dadurch funktionieren relative Pfade nicht mehr, z.B. bei einem Aufruf von File.exists oder ClassLoader.getResource.

Lösung:
Siehe https://maven.apache.org/surefire/maven-surefire-plugin/examples/debugging.html: Maven wird über die Kommandozeile mit dem Parameter -Dmaven.surefire.debug (für "normale" Unittests) bzw. -Dmaven.failsafe.debug (für Integrationstests) gestartet. Bei mir sieht der Aufruf dann so aus:

mvn -Parq-remote -Dmaven.failsafe.debug verify
Jetzt stoppt Maven die Testausführung mit dieser Ausgabe:
Listening for transport dt_socket at address: 5005
Maven listening for debugger

Also legt man in Eclipse eine Debug Configuration vom Typ "Remote Java Application" an und verbindet sich an Port 5005:
Remote Java Application
Sobald der Debugger sich verbunden hat, geht der Maven-Lauf weiter und man kann in Eclipse debuggen.


Ausführen mit Java 17

Im Profil "arq-managed" wird es beim Ausführen mit Java 17 eine Fehlermeldung geben:
19:33:11,840 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-8) MSC000001: Failed to start service org.wildfly.clustering.infinispan.cache-container-configuration.ejb: 
	org.jboss.msc.service.StartException in service org.wildfly.clustering.infinispan.cache-container-configuration.ejb: java.lang.ExceptionInInitializerError
	at org.wildfly.clustering.service@26.0.1.Final//org.wildfly.clustering.service.FunctionalService.start(FunctionalService.java:66)
	at org.jboss.msc@1.4.13.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1739)
	at org.jboss.msc@1.4.13.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.execute(ServiceControllerImpl.java:1701)
	at org.jboss.msc@1.4.13.Final//org.jboss.msc.service.ServiceControllerImpl$ControllerTask.run(ServiceControllerImpl.java:1559)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.ExceptionInInitializerError
	...
	... 8 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.lang.Class java.util.EnumMap.keyType accessible: module java.base does not "opens java.util" to unnamed module @af418a8
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
	at org.wildfly.clustering.marshalling.protostream@26.0.1.Final//org.wildfly.clustering.marshalling.protostream.util.EnumMapMarshaller$1.run(EnumMapMarshaller.java:53)
	at org.wildfly.clustering.marshalling.protostream@26.0.1.Final//org.wildfly.clustering.marshalling.protostream.util.EnumMapMarshaller$1.run(EnumMapMarshaller.java:48)
	at org.wildfly.security.elytron-base@1.18.3.Final//org.wildfly.security.manager.WildFlySecurityManager.doUnchecked(WildFlySecurityManager.java:838)
	at org.wildfly.clustering.marshalling.protostream@26.0.1.Final//org.wildfly.clustering.marshalling.protostream.util.EnumMapMarshaller.(EnumMapMarshaller.java:48)
	... 30 more
Zwei Workaround sind hier zu finden:
https://developer.jboss.org/thread/279810

Workaround 1:
Es wird eine Property "javaVmArguments" zu "arquillian.xml" und dem Container "managed" zugefügt (der vom Profil "arq-managed" verwendet wird):
    <container qualifier="managed">
        <configuration>
            <property name="javaVmArguments">--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED</property>
        </configuration>
    </container>
Eigentlich könnte man auch den "--add-opens"-Teil der WildFly-Befehlszeile kopieren, die ausgeführt wird beim Start über "standalone.bat". Aber diese zwei Stücke sind auf jeden Fall erforderlich, um das Beispiel auszuführen.

Workaround 2:
In "StatelessMaven/pom.xml" wird die Version des Plugins "org.wildfly.arquillian:wildfly-arquillian-container-managed" überdefiniert, die per Default aus dem WildFly-BOM vorbelegt ist (Für WildFly 26.x: 3.0.1.Final). Oben verlinkter Post deklariert eine Version "2.2.0.Final" als funktionsfähig, diese klappte aber bei mir nicht - eventuell weil der Post sich noch auf Java 11 bezieht. Aber es funktioniert mit neueren Versionen, z.B. aktuell mit "5.0.0.Alpha6":
    <profiles>
        <profile>
            <id>arq-managed</id>
            <dependencies>
                <dependency>
                    <groupId>org.wildfly.arquillian</groupId>
                    <artifactId>wildfly-arquillian-container-managed</artifactId>
                    <version>5.0.0.Alpha6</version>
                    <scope>test</scope>
                </dependency>
            </dependencies>
Da diese Version im BOM von WildFly 27 automatisch gesetzt ist, wird der Workaround dort nicht mehr nötig sein.


Wie der Unit-Test abläuft

Der oben gezeigte Aufruf der ShrinkWrap-API erzeugt ein Archiv, das vom Arquillian-Framework auf den Anwendungsserver deployed wird. Vor dem Deploy erweitert das Arquillian-Framework dieses Archiv um "Dinge": Man werfe einen Blick in "pom.xml" des Webprojekts:
		<dependency>
			<groupId>org.jboss.arquillian.protocol</groupId>
			<artifactId>arquillian-protocol-servlet</artifactId>
			<scope>test</scope>
		</dependency>
Diese Dependency findet sich hier:
https://repo1.maven.org/maven2/org/jboss/arquillian/protocol/arquillian-protocol-servlet/1.6.0.Final/. In dieser Datei findet sich unter "org\jboss\arquillian\protocol\servlet\v_3" eine Datei "web-fragment.xml", die ein Servlet "ArquillianServletRunner" definiert.

Mit einem Trick kann man die EAR-Datei anschauen, die vom Unit-Test-Framework auf eine bereits laufenden WildFly-Server deployed wird (geschickterweise nimmt man hier das Remote-Profil, da man dann eher in den Server schauen kann): man fügt in eine beliebige @Test-Methode ein ausreichend langes Thread.Sleep ein. Dann hat man Zeit, um sich den Pfad zur deployten EAR-Datei aus "standalone.xml" zu holen und die Datei im Unterverzeichnis "%WILDFLY_HOME%\standalone\data\content\..." zu finden. Diese Datei schauen wir uns an:

In der EAR-Datei selbst findet sich ein neues Verzeichnis "lib", in dem viele Arquillian-Jars liegen:
EAR-Datei
In dem EAR befindet sich natürlich auch unsere "war"-Datei:
WAR-Datei
Unter "WEB-INF\lib" befindet sich eine Datei "arquillian-protocol.jar". Schauen wir uns diese Datei an, sehen wir dass sie unter "META-INF" die schon bekannte "web-fragment.xml" enthält, die das Servlet "org.jboss.arquillian.protocol.servlet.runner.ServletTestRunner" definiert. Wir können also davon ausgehen, dass in unserer Webanwendung unter http://127.0.0.1:8080/StatelessMaven-web/ArquillianServletRunner ein bisher unbekannter Gast antwortet.

Aus den Teilen der eingangs genannten Dependency "arquillian-protocol-servlet-1.6.0.Final.jar" wird also eine JAR-Datei gebaut, die in unsere EAR-Anwendung injiziert wird. Das erklärt auch, warum in "arquillian.xml" folgendes Element steht:

	<defaultProtocol type="Servlet 3.0" />
Hiermit wird das Verfahren definiert, mittels dem die Client-Seite des Arquillian-Framework sich mit der serverseitigen Gegenseite verbindet: es wird der Pfad "org\jboss\arquillian\protocol\servlet\v_3" aus "arquillian-protocol-servlet-1.6.0.Final.jar" aktiv.

Beim Blick in die Klasse "org.jboss.arquillian.protocol.servlet.runner.ServletTestRunner" bzw. deren Methode "execute" stellen wir fest, dass über einen simplen GET-Request und ein paar HTML-Parameter definiert werden kann, welche Testmethode aufgerufen werden soll.

Die URL für eine der beiden Methoden meines Unit-Test-Beispiels sieht also so aus:
http://127.0.0.1:8080/StatelessMaven-web/ArquillianServletRunner?outputMode=serializedObject&className=de.hsrm.cs.javaee8.statelessmaven.web.test.GeometricModelBeanIT&methodName=testCuboidVolume
Sie ist am einfachsten aufrufbar, indem man die EAR-Datei, die ich mir weiter oben durch den "Thread.sleep"-Trick vom Server gezogen hatte, erneut auf den Server deployed.
Mit dem Ergebnis kann man leider nicht viel anfangen, es ist ein binär serialiertes Java-Objekt, das die Ergebnisse des Aufrufs der Testmethode (Erfolg, Assertion-Verletzungen oder Exceptions) enthält.
Den Parameter "outputMode" kann man im Moment nur auf "serializedObject" stellen. Im Quellcode findet man einen Zweig, über dem "// TODO: implement a html view of the result" steht - schade.


Stand 23.04.2023
Historie:
13.02.2018: erstellt
07.01.2019: WildFly 15, Hinweis: keine Umlaute in "arquillian.xml"
16.02.2019: diverse Plugins aktualisiert
15.01.2020: Projekt auf Basis des Archetype "wildfly-jakartaee-ear-archetype" erstellt - dadurch weniger Nacharbeiten an "pom.xml" nötig, Profile "arq-managed" startet jetzt Server aus "JBOSS_HOME"-Variable, WildFly 18
15.01.2022: WildFly 26, JavaEE8
16.01.2023: Hinweis zu Fehler bei Java 17
23.04.2023: Debugging der UnitTests bei Maven-Ausführung.