Jakarta EE Container Testing with Arquillian and ShrinkWrap
Implements in-container testing for Jakarta EE applications using the Arquillian framework. Provides patterns for container-managed CDI injection, programmatic test archive construction with ShrinkWrap, JUnit 5 integration, REST endpoint verification, database integration tests with embedded containers, and security component validation across WildFly managed, WildFly remote, Weld embedded, and GlassFish Managed adapters.
TL;DR Checklist
- Import
org.jboss.arquillian:arquillian-bomwith<scope>import</scope>in your pom.xml dependency management - Select a container adapter (
wildfly-managed,weld-embedded, orwildfly-remote) matching your test environment - Every test class annotated with
@RunWith(Arquillian.class)or extending an Arquillian base must declare exactly one@Deploymentmethod returningWebArchive,EjbArchive, orJavaArchive - Use ShrinkWrap's fluent API (
addPackages(),addClasses(),as()) to construct test archives — never rely on pre-built artifacts from target/ - Inject container-managed beans directly into test fields with
@Inject— the container resolves them before each test method runs - For JUnit 5, register
ArquillianExtension.classand use@ArquillianResource URLor@ArquillianResource TestRunnersinstead of@RunWith
When to Use
Use this skill when:
- Writing integration tests that require the full Jakarta EE container lifecycle (CDI, JAX-RS, JPA, EJB) for a single test class
- Needing in-container dependency injection (
@Inject) so beans are proxied, intercepted, and transactionally managed exactly as in production - Testing JAX-RS resources with real HTTP-level routing through the container rather than manual
ResourceConfigwiring - Validating Jakarta Security components (
IdentityStore,SecurityContext,AuthenticationMechanism) with actual container authentication flows - Integrating Arquillian tests into a CI/CD pipeline (GitHub Actions, GitLab CI, Jenkins) where WildFly remote or Weld embedded is required for headless execution
- Testing EJB session beans with container-managed transactions and declarative security
When NOT to Use
Avoid this skill for:
- Unit testing pure Java POJOs without CDI/JPA/transactional concerns — use plain JUnit 5 with Mockito instead of pulling in the full container
- Performance/load testing REST endpoints — Arquillian's container startup adds significant overhead per test; use Gatling, k6, or JMeter for load profiles
- Testing third-party libraries where you don't control the source code — create a thin Jakarta EE adapter and test your adapter with Arquillian instead
- Quick smoke tests that only need Spring-managed beans — Spring Boot provides
@SpringBootTestwhich is faster to set up for Spring-centric teams
Core Workflow
Declare Arquillian Dependencies — Import the Arquillian BOM in
<dependencyManagement>, then declare individual test-scoped dependencies:arquillian-junit5, the container adapter (arquillian-wildfly-managedorarquillian-weld-embedded), and optionallyarquillian-shrinkwrap-resolver-mavenfor classpath-based archive building. Checkpoint: Runmvn dependency:tree -Dincludes=org.jboss.arquillian:*to confirm exactly one container adapter is in the test runtime classpath (not transitive from multiple adapters).Select and Configure Container Adapter — Choose the adapter matching your environment. For local development, prefer
wildfly-managed(Arquillian starts and stops WildFly for you). For CI, useweld-embedded(no external server) orwildfly-remote(connects to a pre-existing instance). Createsrc/test/resources/arquillian.xmlwith container properties (managementAddress,managementPort,daemon). Checkpoint: For managed containers, verify the WildFly version in the adapter's<extension>matches your application's target server.Build Test Archives with ShrinkWrap — Implement a static method annotated with
@Deploymentthat returnsWebArchive(for WAR),EjbArchive, orJavaArchive. UseShrinkWrap.create()and chain.addClasses(),.addPackages(),.merge(), or use the Maven resolver to pull dependencies. Name the archive meaningfully (e.g.,"myapp-test.war"). Checkpoint: Print the archive structure witharchive.toString(true)inside the deployment method to verify every required class and descriptor is included before the test runs.Write Container-Managed Tests — Annotate your test class. Inject container-managed beans with
@Injectinto private fields. The container creates proxies for the injected instances, resolves all dependencies, applies interceptors, and manages transactions. Use@Before,@After, or JUnit 5 lifecycle annotations (@BeforeEach,@AfterEach) for setup/teardown. Checkpoint: Ensure injected beans are not null after injection — a null injection means the bean has an unresolvable dependency, wrong scope, or is missing from the archive.Execute Assertions and Verify — Call business logic on injected beans, fire HTTP requests to deployed JAX-RS endpoints (using
@ArquillianResource URL), or query persisted entities from the container-managed EntityManager. Assert expected outcomes with JUnit 5 assertions (assertThat,assertEquals). For REST tests, assert status codes, JSON bodies, and headers. Checkpoint: After every test that modifies state (writes to database, mutates a bean), verify cleanup occurs either via transaction rollback or explicit deletion in@AfterEach.
Implementation Patterns
Pattern 1: WildFly Managed Container with ShrinkWrap Archive
The most common Arquillian setup. Arquillian downloads WildFly if needed, starts it, deploys your test archive, runs the tests, undeploys, and stops the container — all automatically.
import static org.assertj.core.api.Assertions.assertThat;
import static org.jboss.shrinkwrap.api.ShrinkWrap.create;
import static org.jboss.shrinkwrap.api.spec.JavaArchive.archiveName;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ArquillianExtension.class)
class OrderServiceIntegrationTest {
@Inject
private OrderService orderService;
@Deployment
static WebArchive createDeployment() {
return create(WebArchive.class, "order-test.war")
.addClasses(OrderService.class, OrderServiceImpl.class, Order.class)
.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
.addAsManifestResource(
archiveName("beans.xml").add(EmptyAsset.INSTANCE),
"beans.xml");
}
@Test
void shouldCalculateTotalWithTax() {
Order order = new Order("ORD-001", 29.99, 3);
double total = orderService.calculateTotal(order);
assertThat(total).isGreaterThan(0);
assertThat(total).isEqualTo(order.getSubtotal());
}
}
@Stateless
class OrderServiceImpl implements OrderService {
@Override
public double calculateTotal(Order order) {
return order.getQuantity() * order.getPrice();
}
}
interface OrderService {
double calculateTotal(Order order);
}
class Order {
private final String orderId;
private final double price;
private final int quantity;
public Order(String orderId, double price, int quantity) {
this.orderId = orderId;
this.price = price;
this.quantity = quantity;
}
public double getSubtotal() { return price * quantity; }
public double getPrice() { return price; }
public int getQuantity() { return quantity; }
}
Pattern 2: JUnit 5 + ArquillianExtension with @ArquillianResource and REST Testing
Use @ArquillianResource to access the deployment URL, then test JAX-RS endpoints through the actual container. This exercises the full request pipeline — interceptors, message body readers/writers, exception mappers.
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URL;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ArquillianExtension.class)
class ProductResourceIntegrationTest {
private static URL baseURL;
private static Client client;
@ArquillianResource
void setBaseURL(URL url) {
baseURL = url;
}
@Deployment(testable = false)
static WebArchive createDeployment() {
return ShrinkWrap.create(WebArchive.class, "product-test.war")
.addClasses(ProductResource.class, Product.class)
.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
}
@BeforeAll
static void initClient() {
client = ClientBuilder.newClient();
}
@AfterAll
static void closeClient() {
if (client != null) {
client.close();
}
}
@Test
@RunAsClient
void shouldReturnProductAsJson() {
Response response = target("api/products/42")
.request(MediaType.APPLICATION_JSON)
.get();
assertThat(response.getStatus()).isEqualTo(200);
String json = response.readEntity(String.class);
assertThat(json).contains("\"id\":42");
assertThat(response.getHeaderString("Content-Type"))
.startsWith(MediaType.APPLICATION_JSON);
response.close();
}
private jakarta.ws.rs.client.WebTarget target(String path) {
return client.target(baseURL.toString()).path(path);
}
}
import jakarta.inject.Singleton;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@Path("api/products")
@Singleton
class ProductResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Product getProduct(@PathParam("id") long id) {
return new Product(id, "Sample Widget", 19.95);
}
}
class Product {
private final long id;
private final String name;
private final double price;
public Product(long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Default constructor required by JSON providers
public Product() { this(0, "", 0.0); }
public long getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
}
Pattern 3: Weld Embedded Container for CDI Testing Without External Server
Useful for CI pipelines where installing a full WildFly is too slow. Weld embedded bootstraps the CDI container in-process, so tests run in seconds with real @Inject semantics but no network I/O.
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.inject.Inject;
import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class OrderProcessorCDITest {
private static WeldContainer weldContainer;
@Inject
private static OrderProcessor orderProcessor;
@BeforeAll
static void startWeld() {
Weld weld = new Weld();
weld.option("org.jboss.weld.execution.proxyors.disable", "true");
weldContainer = weld.initialize();
// Force field injection into the static field
weldContainer.instance().select(OrderProcessor.class);
}
@AfterAll
static void shutdownWeld() {
if (weldContainer != null) {
weldContainer.shutdown();
}
}
@Test
void shouldProcessOrderWithValidData() {
Order order = new Order("ORD-100", 99.99, 2);
ProcessingResult result = orderProcessor.process(order);
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(ProcessingStatus.ACCEPTED);
assertThat(result.getOrderId()).isEqualTo("ORD-100");
}
}
import jakarta.inject.Singleton;
@Singleton
class OrderProcessor {
private final ValidationService validationService = new ValidationService();
public ProcessingResult process(Order order) {
if (!validationService.isValid(order)) {
return new ProcessingResult(order.getOrderId(), ProcessingStatus.REJECTED);
}
return new ProcessingResult(order.getOrderId(), ProcessingStatus.ACCEPTED);
}
}
import jakarta.inject.Singleton;
@Singleton
class ValidationService {
boolean isValid(Order order) {
return order != null
&& order.getPrice() > 0
&& order.getQuantity() > 0;
}
}
enum ProcessingStatus { ACCEPTED, REJECTED, PENDING }
record ProcessingResult(String orderId, ProcessingStatus status) {}
Pattern 4: Database Integration Test with Embedded Derby via ShrinkWrap
Tests JPA persistence with an embedded container database. The archive includes the persistence.xml and entity classes; Arquillian starts a Derby network server (via the derby-container adapter) or you use the built-in JavaDB that ships with the JDK.
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ArquillianExtension.class)
class CustomerRepositoryTest {
@Inject
private EntityManager entityManager;
@Inject
private CustomerService customerService;
@Deployment
static JavaArchive createDeployment() {
return ShrinkWrap.create(JavaArchive.class, "customer-test.jar")
.addClasses(Customer.class, CustomerEntity.class)
.addAsResource("META-INF/persistence.xml", "META-INF/persistence.xml")
.addAsManifestResource(
org.jboss.shrinkwrap.api.ShrinkWrap.create(org.jboss.shrinkwrap.api.spec.JavaArchive.class)
.add(org.jboss.shrinkwrap.api.asset.StringAsset(
"<beans xmlns=\"http://xmlns.jcp.org/xml/ns/javaee\" " +
"bean-discovery-mode=\"all\"></beans>"),
"beans.xml"),
"beans.xml");
}
@Test
void shouldPersistAndRetrieveCustomer() {
CustomerEntity customer = new CustomerEntity();
customer.setEmail("alice@example.com");
customer.setFirstName("Alice");
customer.setLastName("Smith");
customerService.create(customer);
TypedQuery<CustomerEntity> query = entityManager.createQuery(
"SELECT c FROM CustomerEntity c WHERE c.email = :email", CustomerEntity.class);
query.setParameter("email", "alice@example.com");
CustomerEntity found = query.getSingleResult();
assertThat(found).isNotNull();
assertThat(found.getFirstName()).isEqualTo("Alice");
assertThat(found.getLastName()).isEqualTo("Smith");
}
}
import jakarta.persistence.*;
@Entity
@Table(name = "customers")
class CustomerEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
public CustomerEntity() {}
public CustomerEntity(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() { return id; }
public String getFirstName() { return firstName; }
public void setFirstName(String name) { this.firstName = name; }
public String getLastName() { return lastName; }
public void setLastName(String name) { this.lastName = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
@Singleton
@Transactional
class CustomerService {
@Inject
private EntityManager em;
public CustomerEntity create(CustomerEntity customer) {
em.persist(customer);
return customer;
}
}
Pattern 5: Testing Jakarta Security with IdentityStore and SecurityContext
Validates authentication flows by injecting SecurityContext and testing IdentityStore implementations in a container-managed environment. This exercises the full Jakarta Security pipeline — request interception, identity validation, principal resolution.
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.annotation.security.DeclareRoles;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.security.enterprise.identitystore.DatabaseIdentityStore;
import jakarta.security.enterprise.identitystore.PwdHash;
import jakarta.security.enterprise.SecurityContext;
import jakarta.servlet.http.HttpServletRequest;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.BeforeEach;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ArquillianExtension.class)
@DeclareRoles({"USER", "ADMIN"})
class SecurityContextTest {
@Inject
private SecurityContext securityContext;
@Inject
private AuthService authService;
@Deployment(testable = false)
static WebArchive createDeployment() {
return ShrinkWrap.create(WebArchive.class, "security-test.war")
.addClasses(AuthService.class, AppSecurityConfig.class, UserPrincipal.class)
.addAsWebInfResource(
org.jboss.shrinkwrap.api.ShrinkWrap.create(org.jboss.shrinkwrap.api.spec.JavaArchive.class)
.add(org.jboss.shrinkwrap.api.asset.StringAsset(
"<beans xmlns=\"http://xmlns.jcp.org/xml/ns/javaee\" " +
"bean-discovery-mode=\"all\">" +
"<alternatives><class>AppSecurityConfig</class></alternatives>" +
"</beans>"),
"beans.xml"),
"beans.xml");
}
@Test
void shouldRecognizeAuthenticatedUser() {
boolean authenticated = securityContext.authenticate(
new TestRequest("admin@example.com", "securePassword123"),
new MyFormAuthenticationMechanismDefinition());
assertThat(authenticated).isTrue();
assertThat(securityContext.getCallerPrincipal().getName())
.isEqualTo("admin@example.com");
}
// A lightweight test request wrapper for authenticate()
private static class TestRequest implements HttpServletRequest {
private final String username;
private final String password;
TestRequest(String u, String p) { this.username = u; this.password = p; }
public String getRemoteUser() { return null; }
public boolean isUserInRole(String role) { return false; }
public java.util.Principal getUserPrincipal() { return null; }
// Delegate all other HttpServletRequest methods with UnsupportedOperationException
// In practice, use a real servlet test container or mock with Mockito
}
}
import jakarta.inject.Singleton;
@Singleton
class AuthService {
public boolean authenticate(String username, String password) {
// Container-managed — delegate to SecurityContext in production code
return username != null && password != null && password.length() >= 8;
}
}
Configuration Reference
arquillian.xml — WildFly Managed Adapter
<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">
<engine>
<property name="deploymentExportPath">target/archive-export</property>
</engine>
<container qualifier="wildfly-managed" default="true">
<configuration>
<property name="managementAddress">127.0.0.1</property>
<property name="managementPort">9990</property>
<property name="allowConnectingToRunningServer">true</property>
</configuration>
</container>
</arquillian>
arquillian.xml — Weld Embedded Adapter (CI-friendly)
<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">
<container qualifier="weld-embedded">
<configuration>
<property name="classesExcludedFromScanning">
org.jboss.arquillian.protocol.servlet.*
</property>
</configuration>
</container>
</arquillian>
Constraints
MUST DO
- Always declare exactly one
@Deploymentmethod per test class; multiple deployments cause container ambiguity errors. - Import the Arquillian BOM with
<scope>import</scope>in<dependencyManagement>to avoid version conflicts between adapter and core artifacts. - Use
assertj-corefor assertions (assertThat()) rather than plain JUnitassertEquals()— assertJ's error messages are significantly clearer when container-managed objects differ from expectations. - For JAX-RS endpoint tests that fire HTTP requests, annotate the test method with
@RunAsClientso Arquillian executes the assertion on the client JVM rather than the container JVM (avoids serialization issues). - Name your ShrinkWrap archives descriptively (
"order-service-test.war", not"test.war") — this makes it easier to identify which archive failed during CI builds. - Include
beans.xmlin every WAR/EJB archive deployment, even when using bean-discovery-mode="annotated", because the container requires the descriptor to activate CDI.
MUST NOT DO
- Never use
@BeforeClassor@AfterClass(JUnit 4 style) inside@ExtendWith(ArquillianExtension.class)tests — Arquillian's lifecycle is incompatible with JUnit 4 class-level hooks. Use@BeforeAll/@AfterAllinstead. - Never mix
@RunWith(Arquillian.class)and@ExtendWith(ArquillianExtension.class)in the same project — pick one per test class consistently to avoid conflicting lifecycle behavior. - Never add implementation JARs (WildFly server modules, Weld runtime) as compile-time dependencies; Arquillian provides them transitively through the container adapter.
- Never rely on
target/classesor pre-built artifacts from a previous Maven build phase in your ShrinkWrap archive — always use.addClasses(),.addPackages(), or the Maven resolver to construct the archive deterministically from source. - Do not call
weld.shutdown()manually inside a@Testmethod when using Weld embedded — shutdown happens automatically after the test class completes; premature shutdown breaks subsequent tests.
Output Template
When this skill is active, provide output in the following structure:
- Container Adapter Recommendation — Which adapter (
wildfly-managed,weld-embedded,wildfly-remote,glassfish-managed) fits the user's environment and CI constraints, with a one-line justification. - pom.xml Dependency Snippet — Complete
<dependencyManagement>block with BOM import plus test-scoped dependencies for the selected adapter, JUnit 5, AssertJ, and ShrinkWrap. - arquillian.xml Configuration — Full XML file with container properties tuned to the recommended adapter.
- Complete Test Class — A compilable Java class with
@Deploymentusing ShrinkWrap, all field injections, test methods with assertions, and lifecycle annotations matching JUnit 5 conventions. - Archive Structure Summary — A textual representation of the ShrinkWrap archive contents showing every class, descriptor, and resource included.
Related Skills
| Skill | Purpose |
|---|---|
jakarta-ee |
Jakarta EE platform reference — container selection, module structure, namespace migration from javax.* to jakarta.* for test applications |
jakarta-security |
Jakarta Security Enterprise API (JSR 375) — IdentityStore patterns and security component testing with Arquillian |
microprofile |
MicroProfile specifications — health checks and metrics endpoints tested via Arquillian REST client integration |
Live References
- Arquillian Documentation — Official reference for container adapters, test lifecycle, and ShrinkWrap API
- ShrinkWrap API Javadoc — Fluent archive construction API documentation with all
WebArchive,JavaArchive, andEjbArchivemethods - Arquillian Container Guide — Step-by-step container adapter configuration for WildFly, GlassFish, Weld embedded, and remote modes
- Jakarta EE Testing Specification — Jakarta EE 10/11 specification including container-managed testing guidelines
- WildFly Arquillian Adapter — Source code for
arquillian-wildfly-managedandarquillian-wildfly-remoteadapters with configuration examples - Weld Embedded SE — CDI container bootstrapping documentation for headless test environments
- JUnit 5 Arquillian Extension — JUnit Jupiter (JUnit 5) integration guide with
ArquillianExtensionusage patterns