Sample: Unit test of the JSF layer using Maven and Arquillian Warp


Content:

Arquillian Warp
The JSF side: backing bean
The JSF side: JSF page
The JSF side: Taglibs in pom.xml
Adding Arquillian Warp to pom.xml
Test class
Executing application and test with Java 17
Details of Arquillian Warp


This sample is targeted for WildFly 36: it contains an EAR project, which consists of an EJB project and a Web project. The web project has a Jakarta Faces page and a JSF backing bean, which makes calls to an EJB. The state of the JSF backing bean is unit tested by using the "Arquillian Warp" framework. All is done with Maven.

This sample is based completely on the sample Unit-Test der Webschicht mit Maven und Arquillian Drone and extends it with a JSF test. So take a look at this sample and its predecessing samples. Compared to the previous sample, I removed the servlet GeometricModelServlet and the tests GeometricModelBeanIT and ServletIT. So actually, only the EJB GeometricModelBean, "faces-config.xml", "beans.xml" and all the "pom.xml" files are the same.

The previous sample currently uses WildFly 35, but I migrated this sample to WildFly 36 to avoid an error which is caused by a broken arquillian version 1.9.2 that is brought by the WildFly 35 BOM:
    <properties>
	    ...
        <version.wildfly.bom>36.0.0.Final</version.wildfly.bom>
		...
    </properties>

Here is the zipped Ecplise project for download: StatelessWarp.zip. The guide on how to import it to Eclipse is found in the Stateless Session Bean und Maven sample.


Arquillian Warp

Arquillian Warp (http://arquillian.org/arquillian-extension-warp/) is an Arquillian component which is used for testing JSF backing beans: it hooks into the JSF framework on the server side and allows validation of backing bean values during a request handling.

Some links to Arquillian Warp:
Sources on GitHub: https://github.com/arquillian/arquillian-extension-warp
Blog: http://arquillian.org/blog/tags/warp/

The Arquillian Warp unit tests helped me to create this sample. They can be found here: https://github.com/arquillian/arquillian-extension-warp/blob/master/extension/jsf-ftest/src/test/java/org/jboss/arquillian/warp/jsf/ftest/. The tests BasicJsfTest and lifecycle/TestJsfLifecycle are the most interesting ones.

To run those tests: first download the git repository. Then move into the directory "arquillian-extension-warp-master" and run the following maven commands (you have to specify a maven profile for each one).
E.g. for running them in a managed WildFly 35 server (which means that the Arquillian test runner downloads the WildFly server and starts/stops it himself):
set JAVA_HOME=C:\Program Files\Java\jdk-17
c:\path\to\apache-maven-3.9.9\bin\mvn.cmd install -Pwildfly-managed

Now the full Arquillian Warp package is build, installed to the local Maven repository, and the unit tests are executed.

As an alternative, you could start a WildFly 35 server locally und use the remote profile:
set JAVA_HOME=C:\Program Files\Java\jdk-17
c:\temp\apache-maven-3.9.9\bin\mvn.cmd install -Pwildfly-remote


The JSF side: backing bean

In the project "StatelessMaven-web", add a class de.hsrm.jakartaee.knauf.statelessmaven.web.GeometricModelHandler, which contains the following code:
package de.hsrm.jakartaee.knauf.statelessmaven.web;

import jakarta.annotation.PostConstruct;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;

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

@Named
@RequestScoped
public class GeometricModelHandler
{
  @EJB
  private GeometricModelLocal geometricModelLocal;

  private double dblA = 0;
  private double dblB = 0;
  private double dblC = 0;
  
  public double getA()
  {
    return dblA;
  }
  public void setA(double dblA)
  {
    this.dblA = dblA;
  }

  public double getB()
  {
    return dblB;
  }
  public void setB(double dblB)
  {
    this.dblB = dblB;
  }

  public double getC()
  {
    return dblC;
  }
  public void setC(double dblC)
  {
    this.dblC = dblC;
  }
  
  private double dblSurface = 0;
  private double dblVolume = 0;
  
  public double getVolume()
  {
    return this.dblVolume;
  }
  
  public double getSurface()
  {
    return this.dblSurface;
  } 
  
  public String calculate()
  {
    this.dblVolume = this.geometricModelLocal.computeCuboidVolume(this.dblA, this.dblB, this.dblC);
    this.dblSurface = this.geometricModelLocal.computeCuboidSurface(this.dblA, this.dblB, this.dblC);
    
    return null;
  } 
  
  @PostConstruct
  public void postContruct() 
  {
    System.out.println("GeometricModelHandler.postConstruct");
  }
  
  @Override
  public String toString()
  {
    return "a: " + this.dblA + ", b: " + this.dblB + ", c: " + this.dblC + ", volume: " + this.dblVolume + ", surface: " + this.dblSurface;
  }
}
Details of this class:

Fields/Properties:
The class has member variables and getter/setter properties for the cuboid side lengths "a", "b" and "c". It has also getter for the calculated volume and surface. The fields "a", "b" and "c" will be bound to input fields in the JSF page, and the result values will be written to output fields.

Injected EJB
The worker method uses the EJB GeometricModelBean to calculate the cuboid volume. It is in inject by the server.

Worker method
The method calculate is called when the submit button in the JSF form is clicked. It calculates the volume and surface based on the current values of "a", "b" and "c" and writes it to the fields "volume" and "surface".
The return value of this method does not matter as this simple sample does not contain navigation rules: after clicking the submit button, the same JSF page is called again.

Annotations
The class is annotated with javax.inject.Named and javax.enterprise.context.RequestScoped.

toString/postConstruct
The toString method exits for debugging: it is used in the unit test to display the current state of the backing bean.
Same applies to the method annotated with javax.annotation.PostConstruct: it just does a debugging output.


The Jakarta Faces side: JSF page

Add a JSF page "geometricmodel.xhtml" to "src\main\webapp":
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:f="jakarta.faces.core"
      xmlns:h="jakarta.faces.html"
      xmlns:c="jakarta.tags.core">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Simple JSF sample</title>
</head>
<body>
      
<f:view>
  <h:form id="formGeometricModelInput">
    <h:panelGrid columns="3">
      
      <!-- Print calculation results only if last request was a valid calculation. -->
      <c:if test="${geometricModelHandler.volume > 0.0}"> 
        <h:outputText value="Volume:"/>
        <h:outputText id="volume" value="#{geometricModelHandler.volume}"></h:outputText>
        <h:outputText value=""></h:outputText>
      
        <h:outputText value="Surface:"/>
        <h:outputText id="surface" value="#{geometricModelHandler.surface}"></h:outputText>
        <h:outputText value=""></h:outputText>
      </c:if>
      
      <h:outputText value="Side a:"></h:outputText>
      <h:inputText label="Side A" id="a" value="#{geometricModelHandler.a}"></h:inputText>
      <h:message for="a"></h:message>
      
      <h:outputText value="Side b:"></h:outputText>
      <h:inputText label="Side B" id="b" value="#{geometricModelHandler.b}"></h:inputText>
      <h:message for="b"></h:message>
      
      <h:outputText value="Side c:"></h:outputText>
      <h:inputText label="Side C" id="c" value="#{geometricModelHandler.c}"></h:inputText>
      <h:message for="c"></h:message>
      
      <h:commandButton id="calculate" value="Calculate" action="#{geometricModelHandler.calculate}"></h:commandButton>
      <h:outputText value=""></h:outputText>
      <h:outputText value=""></h:outputText>
      
    </h:panelGrid>
  </h:form>
</f:view>
</body>
</html>
The page defines a Form "formGeometricModelInput", which has the input fields for the side lengths "a", "b" and "c". Those are bound to properties of the "geometricModelHandler" backing bean.

The button "Calculate" submits the form and calls geometricModelHandler.calculate. As this method of the backing bean returns null, the same page is displayed after submitting the form.

Now, the results are displayed: by using the JSTL tag c:if, the result fields are only shown when the current volume/surface values are greater than 0, which means that a calculation was done before.

Eclipse will not detect the four taglibs "jakarta.faces.facelets", "jakarta.faces.html", "jakarta.faces.core" and "jakarta.tags.core" by default (though the sample will run fine, as the WildFly server provides them). So you have to modify "pom.xml", see next chapter.


The JSF side: Taglibs in pom.xml

To make Eclipse support the JSTL and JSF taglibs, add those lines to "StatelessMaven-web\pom.xml":

JSTL
	<dependencies>
		...
		<dependency>
			<groupId>jakarta.servlet.jsp</groupId>
			<artifactId>jakarta.servlet.jsp-api</artifactId>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>jakarta.servlet.jsp.jstl</groupId>
			<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>org.glassfish.web</groupId>
			<artifactId>jakarta.servlet.jsp.jstl</artifactId>
			<version>3.0.1</version>
			<scope>provided</scope>
		</dependency>
		...
	</dependencies>
The artifacts "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api" and "org.glassfish.web:jakarta.servlet.jsp.jstl" contain the JSTL tag library. The JSTL api artifact ("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api") is maybe not necessary here, but I added it anyway. The artifact "org.glassfish.web:jakarta.servlet.jsp.jstl" is necessary here, because it contains the tag library files ("*.tld") describing the tag lib. Eclipse parses those files for code completion.

The artifact "jakarta.servlet.jsp:jakarta.servlet.jsp-api" is required here, because it contains base classes for JSP tags. If it is not available, Eclipse cannot resolve any tag at all, and all tag usages will report errors The tag handler class for "c:if" (org.apache.taglibs.standard.tag.rt.core.IfTag) was not found on the Java Build Path. The next version of the project archetype (for WildFly 37) will already contain this dependency, so adding it does not have to be added later.
.
The scope "provided" means that the artifact is needed only at compile time, but the files are bundled with the WildFly server ("%WILDFLY36_HOME%\modules\system\layers\base\jakarta\servlet\jstl\api\main\jakarta.servlet.jsp.jstl-3.0.1-jbossorg-1.jar") and don't need to be added to the deployed WAR file. Most dependencies also don't need a version declaration, as they are contained in the wildfly bom.

Unfortunately, Eclipse will still report an error Can't find facelet tag library for uri jakarta.tags.core (generated by "Facelet Validator"). The "Facelet Validator" is probably not updated to JakartaEE 10.

Eclipse can handle the JakartaEE 10 JSTL only on JSP pages, but not with Facelets.


JSF core/html
The pom.xml of the web project already contains this dependency (points to "%WILDFLY36_HOME%\modules\system\layers\base\jakarta\faces\impl\main\jakarta.faces-4.0.11.jar"), which contains the Jakarta Faces implementation:
        <dependency>
            <groupId>jakarta.faces</groupId>
            <artifactId>jakarta.faces-api</artifactId>
            <scope>provided</scope>
        </dependency>
But when referencing one of the Jakarta Faces tag libraries, Eclipse reports this warning: Can't find facelet tag library for uri jakarta.faces.core (generated by "Facelet Validator"). The reason might be that Eclipse only supports the "Java Server Faces" project facet up to version 2.3, but not the newer Jakarta Faces (see
https://github.com/eclipse-jsf/webtools.jsf/issues/8 and https://bugs.eclipse.org/bugs/show_bug.cgi?id=580851).

Adding Arquillian Warp to pom.xml

First, add the BOM for Arquillian Warp in the root "pom.xml":
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.jboss.arquillian.extension</groupId>
				<artifactId>arquillian-warp-bom</artifactId>
				<version>2.0.0.Final</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
URL:
https://repo.maven.apache.org/maven2/org/jboss/arquillian/extension/arquillian-warp-bom/1.0.0/arquillian-warp-bom-1.0.0.pom


Next, add the "real" dependencies for Arquillian Warp in the web project "pom.xml":
	<dependencies>
		....
		
		<!-- Arquillian Warp basics: -->
		<dependency>
			<groupId>org.jboss.arquillian.extension</groupId>
			<artifactId>arquillian-warp-api</artifactId>
			<scope>test</scope>
		</dependency>
		
		<!-- Arquillian Warp basics: annotations like "BeforePhase" -->
		<dependency>
			<groupId>org.jboss.arquillian.extension</groupId>
			<artifactId>arquillian-warp-jsf</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>


Test class

As in the previous sample, this is an integration test, because the test requires the artifacts of EJB jar and Web war. So the test class name has the extension "IT" (="integration test").

The test class has one change compared to the previous sample: it now has an additional annotation org.jboss.arquillian.warp.WarpTest:
@WarpTest
@ExtendWith(ArquillianExtension.class)
@RunAsClient
public class WarpIT
{
The method which uses the ShrinkWrap api to create a deployable archive (annotated with org.jboss.arquillian.container.test.api.Deployment) is the same as in my previous sample and is not explained any more.

As in the previous sample, a org.openqa.selenium.WebDriver and the deployment url are injected in member variables:
  @Drone
  private WebDriver browser;

  @ArquillianResource()
  private URL deploymentUrl;
Now it is time to do the test. Here is the full code:
import jakarta.inject.Inject;
import org.jboss.arquillian.warp.Activity;
import org.jboss.arquillian.warp.Inspection;
import org.jboss.arquillian.warp.Warp;
import org.jboss.arquillian.warp.jsf.AfterPhase;
import org.jboss.arquillian.warp.jsf.Phase;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

  ...
  @Test
  public final void browserTest() throws Exception
  {
    Warp.initiate(new Activity()
    {
      @Override
      public void perform()
      {
        browser.navigate().to(deploymentUrl.toExternalForm() + "geometricmodel.faces");
      }
    }).inspect(new Inspection()
    {
      private static final long serialVersionUID = 1L;

      // Nothing to be done...

    });

    Warp.initiate(new Activity()
    {
      public void perform()
      {
        WebElement txt = browser.findElement(By.id("formGeometricModelInput:a"));
        // Delete old value, then write new values to the form:
        txt.clear();
        txt.sendKeys("1");

        txt = browser.findElement(By.id("formGeometricModelInput:b"));
        txt.clear();
        txt.sendKeys("2");

        txt = browser.findElement(By.id("formGeometricModelInput:c"));
        txt.clear();
        txt.sendKeys("3");

        // Submit the form:
        WebElement btn = browser.findElement(By.id("formGeometricModelInput:calculate"));
        btn.click();

        WebElement result = browser.findElement(By.id("formGeometricModelInput:volume"));
        Assertions.assertEquals("6.0", result.getText());
        
        result = browser.findElement(By.id("formGeometricModelInput:surface"));
        Assertions.assertEquals("22.0", result.getText());
      }
    })
        .inspect(new Inspection()
        {
          private static final long serialVersionUID = 1L;

          @Inject
          GeometricModelHandler hmb;

          @AfterPhase(Phase.INVOKE_APPLICATION)
          public void afterInvokeApplication()
          {
            Assertions.assertEquals(6, hmb.getVolume(), 0.0, "invalid volume");
            Assertions.assertEquals(22, hmb.getSurface(), 0.0, "invalid surface");
          }
        });
The code in detail:

Step 1: Activity
There are two calls to Warp.initiate: this method returns an org.jboss.arquillian.warp.client.execution.WarpActivityBuilder, which initializes a server request: an anonymous implementation of the org.jboss.arquillian.warp.Activity interface is provided. The interface method perform does some client side processing: using the Arquillian Drone API, you can navigate to web pages, fill forms and submit them.

The first activity in my sample just browses to the JSF page "geometricmode.xhtml":
new Activity()
    {
      @Override
      public void perform()
      {
        browser.navigate().to(deploymentUrl.toExternalForm() + "geometricmodel.xhtml");
      }
    }

And the second activity fills the form fields, clicks the submit button and finally performs a client side assertion to check whether the results in the HTML response are valid:
new Activity()
    {
      public void perform()
      {
        WebElement txt = browser.findElement(By.id("formGeometricModelInput:a"));
        // Delete old value, then write new values to the form:
        txt.clear();
        txt.sendKeys("1");

        txt = browser.findElement(By.id("formGeometricModelInput:b"));
        txt.clear();
        txt.sendKeys("2");

        txt = browser.findElement(By.id("formGeometricModelInput:c"));
        txt.clear();
        txt.sendKeys("3");

        // Submit the form:
        WebElement btn = browser.findElement(By.id("formGeometricModelInput:calculate"));
        btn.click();

        WebElement result = browser.findElement(By.id("formGeometricModelInput:volume"));
        Assertions.assertEquals("6.0", result.getText());
        
        result = browser.findElement(By.id("formGeometricModelInput:surface"));
        Assertions.assertEquals("22.0", result.getText());
      }
    }
In the JSF page, you define a form "formGeometricModelInput" with input elements "a", "b" and "c" and a submit button "calculate". But in the resulting HTML, their IDs contain the full hierarchie: "formGeometricModelInput:a". So you have to use the full qualified name on the client side.


Step 2: Observation
This is not needed in my sample. But if your request returns multiple result files (e.g. images, css files or javascript files), you have to filter the relevant file, because the following inspection should only be performed on the "real" JSF page. To do so, add a call to the observe method.
The following snippet checks that the URL of the current result ends with "geometricmodel.faces" and thus is a valid JSF page:
 .observe(HttpFilters.request().uri().contains("geometricmodel.faces"))

Step 3: Inspection
With the org.jboss.arquillian.warp.client.execution.WarpActivityBuilder (or the result of the observe method), you perform the server side testing code by calling the method inspect. The parameter is an anonymous implementation of the interface org.jboss.arquillian.warp.Inspection. All code in this inspection is executed on the server side. Note the console outputs in my sample.
.inspect(new Inspection()
    {
      private static final long serialVersionUID = 1L;

      // ...
    });
The "serialVersionUID" must be defined on each Inspection implementation. Otherwise, there will be a client side error on test execution:

Unable to transform inspection de.hsrm.jakartaee.knauf.statelessmaven.web.test.WarpIT$1:
serialVersionUID for class de.hsrm.jakartaee.knauf.statelessmaven.web.test.WarpIT$1 is not set; please set serialVersionUID to allow Warp work correctly
Caused by: org.jboss.arquillian.warp.impl.client.transformation.NoSerialVersionUIDException: serialVersionUID for class de.hsrm.jakartaee.knauf.statelessmaven.web.test.WarpIT$1 is not set; please set serialVersionUID to allow Warp work correctly

In the Inspection implementation, you can add several methods used for testing. They all are annotated with either org.jboss.arquillian.warp.jsf.BeforePhase or org.jboss.arquillian.warp.jsf.AfterPhase. This annotation tells Arquillian Warp at which JSF lifecycle phase the method should be called.

          @BeforePhase(Phase.RENDER_RESPONSE)
          public void beforeRenderResponse()
          {
            ...
          }

          @AfterPhase(Phase.RENDER_RESPONSE)
          public void afterRenderResponse()
          {
            ...
          }
The org.jboss.arquillian.warp.jsf.Phase enumeration has these values, which correspond to the JSF lifecycle:
My sample defines methods for all "before" and "after" steps and just prints the state of the backing bean. This shows when the form values are written to the bean fields and when the processing of the "submit" button click is done.

Note: you can also annotate methods with org.jboss.arquillian.warp.servlet.BeforeServlet and org.jboss.arquillian.warp.servlet.AfterServlet. Those are more related to servlets.

Step 3a: Testing
Depending on the phase, you can test the state of e.g. the FacesContext or custom JSF backing beans. To do so, inject the bean in the Inspection implementation:
          @Inject
          GeometricModelHandler hmb;
Now, you can check e.g. the properties "volume" and "surface" of the backing bean (which is done here after the INVOKE_APPLICATION phase - quite late in the lifecycle, but this is the first point where the "submit" button click is handled and the resulting values can be found in the backing bean):
          @AfterPhase(Phase.INVOKE_APPLICATION)
          public void afterInvokeApplication()
          {
            Assertions.assertEquals(6, hmb.getVolume(), 0.0, "invalid volume");
            Assertions.assertEquals(22, hmb.getSurface(), 0.0, "invalid surface");
          }
Here, a volume of 6 and a surface of 22 are expected.
If an assertion fails, the maven test runner will print the error message.


Don't do this: multiple requests in one call to Warp.initiate:

Assume that you write your test code like this:
    Warp.initiate(new Activity()
    {
      @Override
      public void perform()
      {
        browser.navigate().to(deploymentUrl.toExternalForm() + "geometricmodel.faces");
      
        WebElement txt = browser.findElement(By.id("formGeometricModelInput:a"));
		
        // fill form data...

        // Submit the form:
        WebElement btn = browser.findElement(By.id("formGeometricModelInput:calculate"));
        btn.click();

        // check result values...
      }
    })
        .inspect(new Inspection()
        {
          private static final long serialVersionUID = 1L;

          @AfterPhase(Phase.INVOKE_APPLICATION)
          public void afterInvokeApplication()
          {
            // do testing...
          }
        });
This code (which seems to be identical to my working sample) will result in this exception:

org.jboss.arquillian.warp.impl.client.verification.InspectionMethodWasNotInvokedException: Lifecycle test declared on public void de.hsrm.cs.javaee7.statelessmaven.web.test.WarpIT$1.afterInvokeApplication() with qualifiers [@org.jboss.arquillian.warp.jsf.AfterPhase(value=INVOKE_APPLICATION)] was not executed

The reason seems to be: we have two server calls: the first initial request to the JSF page, and the second request when submitting the form.

It seems that Arquillian Warp hooks into the first call, where not all lifecycle phases are passed through.



Executing application and test with Java 17

Both profiles of the sample will fail with Java 17.

This error will occur when executing the "arq-remote" profile:
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 9.071 s <<< FAILURE! - in de.hsrm.jakartaee.knauf.statelessmaven.web.test.WarpIT
[ERROR] browserTest(de.hsrm.jakartaee.knauf.statelessmaven.web.test.WarpIT)  Time elapsed: 2.426 s  <<< ERROR!
org.jboss.arquillian.warp.exception.ClientWarpExecutionException: 
enriching request failed; caused by:
java.lang.reflect.InaccessibleObjectException: Unable to make 
	protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: 
	module java.base does not "opens java.lang" to unnamed module @1d16f93d
Caused by: java.lang.RuntimeException: 
Could not transform and replicate class class java.util.Arrays$ArrayList:
Unable to convert org.jboss.arquillian.warp.generated.A92237c48-2d39-4e24-a50a-8db84ac45218 to class
Caused by: org.jboss.arquillian.warp.impl.client.transformation.InspectionTransformationException: Unable to convert org.jboss.arquillian.warp.generated.A92237c48-2d39-4e24-a50a-8db84ac45218 to class
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make 
	protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: 
	module java.base does not "opens java.lang" to unnamed module @1d16f93d

Normally, you would work around this by adding this argument to the Java call:

java --add-opens java.base/java.lang=ALL-UNNAMED ...
But here, it is a bit more complicated, as it will not work to just add this argument to the maven call. The following line is printed before the error message when activing the maven debug log - no "add-opens" arguments:
[DEBUG] Forking command line: cmd.exe /X /C ""C:\Program Files\Java\jdk-17.0.2\bin\java" -jar C:\Users\USERNAME\AppData\Local\Temp\surefire1268444752194443857\surefirebooter5861924953478671380.jar 
	C:\Users\USERNAME\AppData\Local\Temp\surefire1268444752194443857 2022-03-18T19-18-30_344-jvmRun1 surefire6631879617708667689tmp surefire_016533580584915886779tmp"

A new Java process is forked by the "maven-failsafe-plugin", and here the module system parameter is missing.


The resolution is to add this command line argument as argLine to "pom.xml" of the root project:
    </profiles>
        <profile>
            <id>arq-remote</id>
            ...
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-failsafe-plugin</artifactId>
                        ...
                        <configuration>
                            <systemPropertyVariables>
                                <arquillian.launch>remote</arquillian.launch>
                            </systemPropertyVariables>
                            <argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>


Same workaround is needed for the "arq-managed" profile.


Details of Arquillian Warp

We now take a look at the deployed application. To do so, add a Thread.sleep statement at any part of the test method. Now, you can open "standalone.xml" and search for the path of the deployed application:

    <deployments>
        <deployment name="StatelessMaven-ear.ear" runtime-name="StatelessMaven-ear.ear">
            <content sha1="776227df79bb45cc6cfe22d06689077ec35b9ab3"/>
        </deployment>
    </deployments>
This maps to the path "%WILDFLY_HOME%\standalone\data\content\77\6227df79bb45cc6cfe22d06689077ec35b9ab3\content". Here, you will find a file "content". Rename it to "StatelessMaven-ear.ear" and open it with a zip tool.
See the sample
Beispiel: Unit-Test mit Maven und Arquillian for a basic analysis. The EAR file content is the same, but the war file looks different:
WAR content
Besides the well known "arquillian-protocol.jar", which creates a servlet that tunnels Arquillian calls, two more "arquillian-warp" JARs are added. When taking a look at the file "arquillian-warp-jsf.jar", you find a "META-INF\faces-config.xml" file: this one adds a Arquillian Warp specifiy PhaseListener and a FacesContextFactory.

Here is a description of the way Arquillian Warp hooks into a request: https://github.com/lfryc/arquillian.github.com/blob/warp-docs/docs/warp.adoc#warp-request-processing

Quoted from this page:
In order to hook into client-to-server communication, Warp puts a HTTP proxy in between.

This proxy observes requests incoming from a client and then enhances a request with a payload required for a server inspection (processed reffered to as "piggy-backing on a request").

Once an enhanced request enters a server, it is blocked by a request filter and an inspection is registered into an Arquillian system. The Warp’s filter then handles the processing to the traditional request processing.

During a requst processing lifecycle, the Warp listens for appropriate lifecycle hooks and as a response, it can execute arbitrary actions which inspects a state of the request context.

To help with a full-featured verification, a Warp’s inspection process can leverage Arquillian’s dependency injection system.

Once the request is processed by the server, leading into committing response, Warp can collect a result of inspection and enhance a built response to the client (again using piggy-backing method).

The Warp’s proxy evaluates the response and either reports a failure (in case of server failure) or continues with execution of the test.




Last modified 21.04.2025
History:
21.04.2025: created based on the JavaEE 8 version