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 8 and 11
Executing application and test with Java 17
Details of Arquillian Warp
This sample is targeted for WildFly 26: it contains an EAR project, which consists of an EJB project and a Web project. The web project has a Java Server 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 Jsf23Activator
and all the "pom.xml" files are the same.
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/
Surprisingly, version 1.0.0 was released in 2019, after 3 years of no further development. So let's hope that this project is not dead yet.
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 8 server (which means that the Arquillian test runner downloads the WildFly server and starts/stops it himself):
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202
c:\path\to\apache-maven-3.6.0\bin\mvn.cmd install -Pwildfly-managed-8-0
But this did not work for me:
org.jboss.modules.ModuleNotFoundException: org.jboss.as.standalone:main
at org.jboss.modules.ModuleLoader.loadModule(ModuleLoader.java:240)
at org.jboss.modules.Main.main(Main.java:385)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0
So I started a WildFly 8 server locally und used the remote profile:
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202
c:\temp\apache-maven-3.6.0\bin\mvn.cmd install -Pwildfly-remote-8-0
Now the full Arquillian Warp package is build, installed to the local Maven repository, and the unit tests are executed.
The JSF side: backing bean
In the project "StatelessMaven-web", add a class de.hsrm.cs.javaee8.statelessmaven.web.GeometricModelHandler
, which contains the following code:
package de.hsrm.cs.javaee8.statelessmaven.web;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.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.
Eclipse 2021-12 could not resolve the annotation
@javax.annotation.PostConstruct
.
Resolution: add this to dependencies in "StatelessMaven-web\pom.xml":
<dependency>
<groupId>org.jboss.spec.javax.annotation</groupId>
<artifactId>jboss-annotations-api_1.3_spec</artifactId>
<scope>provided</scope>
</dependency>
WildFly contains this jar file in "%WILDFLY_HOME%\modules\system\layers\base\javax\annotation\api\main\jboss-annotations-api_1.3_spec-2.0.1.Final.jar".
The JSF 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="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:c="http://java.sun.com/jsp/jstl/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 three taglibs "http://java.sun.com/jsf/html", "http://java.sun.com/jsf/core" and "http://java.sun.com/jsp/jstl/core" by default (though the sample will run fine).
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>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
The scope "provided" means that it is needed only at compile time, but the files are bundled with the WildFly server ("%WILDFLY15_HOME%\modules\system\layers\base\javax\servlet\jstl\api\main\taglibs-standard-impl-1.2.6-RC1.jar")
and don't need to be added to the deployed WAR file.
JSF core/html
Those two tag libraries are a bit more complicated:
They can be found in "%WILDFLY_HOME%\modules\system\layers\base\com\sun\jsf-impl\main\jsf-impl-2.3.17.SP01.jar", which has (based on the pom.xml included in the JAR file) the maven groupId "com.sun.faces" and artifactId "jsf-impl",
which should match the URL https://repo.maven.apache.org/maven2/com/sun/faces/jsf-impl/. But version "2.3.17.SP01" is not found in Maven central - the versions here stop at "2.2.20".
It can be found at the JBoss repository: https://repository.jboss.org/nexus/content/groups/public/com/sun/faces/jsf-impl/.
So there are two ways to solve this:
Workaround: use the older version 2.2.20:
<dependencies>
...
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-impl</artifactId>
<version>2.2.20</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
Better: use 2.3.17.SP01, and add the JBoss repository (which is already contained in recent versions of the archetype):
<dependencies>
...
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-impl</artifactId>
<version>2.3.17.SP01</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
<repositories>
<repository>
<id>jboss-public-repository-group</id>
<name>JBoss Public Repository Group</name>
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
</repositories>
Adding Arquillian Warp to pom.xml
First, add the BOM for Arquillian Warp:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-warp-bom</artifactId>
<version>1.0.0</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:
<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
@RunWith(Arquillian.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 javax.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.Assert;
import org.junit.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"));
Assert.assertEquals("6.0", result.getText());
}
})
.inspect(new Inspection()
{
private static final long serialVersionUID = 1L;
@Inject
GeometricModelHandler hmb;
@AfterPhase(Phase.INVOKE_APPLICATION)
public void afterInvokeApplication()
{
Assert.assertEquals("volume ", 6, hmb.getVolume(), 0.0);
Assert.assertEquals("surface", 22, hmb.getSurface(), 0.0);
}
});
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"));
Assert.assertEquals("6.0", result.getText());
result = browser.findElement(By.id("formGeometricModelInput:surface"));
Assert.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.cs.javaee7.statelessmaven.web.test.WarpIT$1:
serialVersionUID for class de.hsrm.cs.javaee7.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.cs.javaee7.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:
- RESTORE_VIEW
- APPLY_REQUEST_VALUES
- PROCESS_VALIDATIONS
- UPDATE_MODEL_VALUES
- INVOKE_APPLICATION
- RENDER_RESPONSE
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()
{
Assert.assertEquals("invalid volume", 6, hmb.getVolume(), 0.0);
Assert.assertEquals("invalid surface", 22, hmb.getSurface(), 0.0);
}
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 8 and 11
The code of the sample defines Java version 11 in "StatelessMaven\pom.xml":
<maven.compiler.release>11</maven.compiler.release>
This version declaration is supported since Java 9, and for previous versions, you have to remove this line and replace it with those two lines:
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
Note: those configurations are just shortcuts to the configuration of "maven-compiler-plugin":
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${version.compiler.plugin}</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
To deploy and run the JSF application: see sample Stateless Session Bean und Maven: run the goals clean install wildfly:deploy
.
You could also set the option "Skip tests" in the launch configuration, but this is not necessary here the sample contains only integration tests which are not executed here.
Then call the URL http://localhost:8080/StatelessMaven-web/geometricmodel.faces
Don't forget to undeploy the application afterwards: run the goal wildfly:undeploy
using the same profile as on deployment.
To run the tests: See sample Unit-Test mit Maven und Arquillian: execute the goal verify
(as my WarpIT
is an integration test),
and launch either the profile arq-managed
or arq-remote
.
To sum it up: both profiles are defined in "StatelessMaven-web\pom.xml". They use the maven-failsafe-plugin
because of the integration tests. The file "StatelessMaven-web\src\test\resources\arquillian.xml"
defines further details about the WildFly server configuration. Most important is this line:
<defaultProtocol type="Servlet 3.0" />
This line is a prerequisite for Arquillian Warp to run.
Executing application and test with Java 17
The sample will fail with Java 17.
The two profiles need different workarounds.
The "arq-remote" profile 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.cs.javaee8.statelessmaven.web.test.WarpIT
[ERROR] browserTest(de.hsrm.cs.javaee8.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>
The "arq-managed" profile with Java 17
You will need the same workaround as before, but this time for the "arq-managed" profile:
<profile>
<id>arq-managed</id>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
...
<configuration>
<systemPropertyVariables>
<arquillian.launch>managed</arquillian.launch>
</systemPropertyVariables>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
But after applying it, another error will occur:
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
The declaration of the Java arguments in "pom.xml" will not work, because here the error occurs when launching WildFly - this is not handled by the "maven-failsafe-plugin" arguments.
Two workarounds can be found at https://developer.jboss.org/thread/279810:
Workaround 1:
Add a property "javaVmArguments" to "arquillian.xml" and the container "managed" (which is used by the profile "arq-managed"):
<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>
Actually, you could also copy the "--add-opens" part that is shown when starting WildFly by using "standalone.bat". But those two arguments are the minimum required to run my sample.
Workaround 2:
In "StatelessMaven/pom.xml" redefine the version of the plugin "org.wildfly.arquillian:wildfly-arquillian-container-managed", which is by default defined in the WildFly BOM (for WildFly 26.x it is "3.0.1.Final").
The linked post states that version "2.2.0.Final" should work, but this failed for me - maybe because the post is about Java 11.
But it works with newer versions, e.g. with the most recent release when writing this article, which is "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>
This version will be declared in the BOM of WildFly 27, so the workaround will not be needed anymore.
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:
Besides the well known "arquillian-protocol.jar", which creates a servlet that tunnels Arquillian calls, three 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.
One thing that irritated me first: after running the tests, a directory "StatelessMaven\StatelessMaven-web\lib\bin\lib\amd64-Windows-gpp\jni" appears, with three DLLs inside ("barchart-udt-core-2.3.0-SNAPSHOT.dll", "libgcc_s_sjlj_64-1.dll", "libstdc++_64-6.dll"). The first one ("barchart-udt") seems to be a "Java wrapper around native C++ UDT protocol implementation" (https://github.com/barchart/barchart-udt/wiki), and it includes the two other DLLs (see https://github.com/barchart/barchart-udt/blob/master/barchart-udt-core/pom.xml). It seems the "barchart-udt" JAR is introduced by Netty ("an asynchronous event-driven network application framework"), and this one is probably
a dependency of Arquillian Warp.
Last modified 01.10.2024
History:
09.02.2019: created
26.01.2020: re-created the project based on the archetype "wildfly-jakartaee-ear-archetype", profile "arq-managed" starts the server by using the "JBOSS_HOME" variable,
updated to WildFly 18, updated to from Arquillian Warp 1.0.0.Alpha8 to 1.0.0.
20.01.2022: update to WildFly 26 and Java 11, workaround for Warp error due to outdated "javassist" library.
18.03.2022: Workarounds to run the sample with Java 17
28.12.2022: converted JSP page to XHTML.
08.01.2023: removed "managed-bean" declaration in "faces-config.xml" and updated documentation to focus only on CDI, updated steps to run the sample on Java 1.8
16.01.2023: updated Workaround for Java 17
01.10.2024: Updated "arquillian-warp" to 1.0.1, which does not require a workaround to run with Java 11.