Using Arquillian For Reliable Java EE Integration Tests
Unit tests are excellent at checking business rules in isolation, but they cannot prove that a Java EE application is correctly wired inside its runtime. Dependency injection, persistence contexts, transaction boundaries, security constraints and REST endpoints all depend on the application server. Arquillian addresses this gap by running tests inside, or against, a real container.
For teams maintaining Jakarta EE applications, the framework provides a consistent way to package selected classes and resources into a deployable archive. The test then exercises the application through its actual container services rather than through a collection of mocks. This makes failures more representative of production behaviour, although the additional infrastructure requires deliberate setup.
The approach remains useful in Australian development teams working across Sydney, Melbourne and Brisbane, where enterprise systems often need to support long-lived Java platforms and strict release controls. It also suits distributed teams working in AEST, where reproducible integration environments reduce the time lost diagnosing differences between local laptops, CI agents and cloud deployments in an AWS Sydney region.
Where Arquillian Fits In The Test Pyramid
Arquillian sits between unit testing and full system testing. A unit test might mock an EntityManager and a CDI service, while an Arquillian test deploys those components to an application server and verifies that they collaborate through the container. It is therefore well suited to testing persistence, transactions, CDI scopes, EJB behaviour, JAX-RS resources and security integration.
The framework separates the test logic from the container lifecycle. A test class can use @RunWith(Arquillian.class) and define a @Deployment method that creates a WAR, JAR or EAR with ShrinkWrap. Arquillian then hands that archive to an adapter for WildFly, Payara, GlassFish or another supported runtime.
This distinction is important when choosing the test target. A mock-heavy test answers whether a method behaves correctly given controlled collaborators. An Arquillian test answers whether the application server can discover, construct and execute the deployed components. Both forms are valuable, but combining them in every test would create slow and unnecessarily complicated feedback.
Building A Focused Deployment Archive
ShrinkWrap is central to a practical Arquillian setup. Instead of deploying the entire application, the test can assemble a small archive containing the production classes, test-specific beans, persistence configuration, and required libraries. A focused deployment starts faster and makes failures easier to interpret.
A typical test might look like this:
@RunWith(Arquillian.class)
public class CustomerRepositoryIT {
@Deployment
public static Archive<?> deployment() {
return ShrinkWrap.create(WebArchive.class, "customers.war")
.addPackages(true, "com.example.customer")
.addAsResource("META-INF/persistence.xml")
.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
}
@Inject
CustomerRepository repository;
@Test
public void storesAndLoadsACustomer() {
repository.save(new Customer("Ada"));
assertThat(repository.findByName("Ada")).isPresent();
}
}
The exact archive should reflect the application’s module boundaries. Including everything from the main project can hide missing dependencies because the test succeeds for accidental reasons. Explicitly adding beans.xml, persistence descriptors and selected packages exposes deployment assumptions early.
Arquillian tests can also use test resources that point to an in-memory H2 database or a dedicated integration database. For PostgreSQL or Oracle-specific behaviour, a container-managed database closer to production is more trustworthy. Teams using Docker in Melbourne or Perth should keep database versions aligned with CI, since a local image upgrade can otherwise produce a misleading green build.
Choosing A Container And Execution Mode
Arquillian adapters commonly support embedded, managed and remote execution. An embedded container starts within the test process and can be convenient for a developer workstation. A managed container is started and stopped by the build, while a remote container is already running and receives the deployment. The choice affects speed, isolation and operational complexity.
Embedded mode is attractive for a quick proof of concept, but it may behave differently from the application server used in production. Managed mode offers stronger repeatability in CI, while remote mode is useful when the server is provisioned by a separate pipeline or shared environment. Remote execution needs careful cleanup, because stale deployments and database rows can make test outcomes depend on execution order.
| Approach | Best Use | Strengths | Common Risk |
|---|---|---|---|
| Embedded container | Fast local feedback | Simple startup and debugging | Runtime differences from production |
| Managed container | Repeatable CI integration tests | Controlled lifecycle and isolation | Longer builds and setup work |
| Remote container | Shared or provisioned environments | Matches an existing server topology | State leakage between test runs |
| Testcontainers | External services and modern CI | Reproducible database or broker versions | More orchestration outside Arquillian |
| Mock-based tests | Business logic and edge cases | Very fast and focused | Misses container wiring and runtime behaviour |
Arquillian does not have to manage every dependency. A team might use it for the Java EE application server and Testcontainers for Kafka, PostgreSQL or an identity provider. This hybrid arrangement is often more realistic than forcing all infrastructure into one tool, particularly when asynchronous processing is part of the system.
Testing Transactions, REST And Asynchronous Workflows
Persistence tests should verify transaction boundaries rather than simply checking that a repository returns an object. An Arquillian test can confirm that a CDI or EJB service commits data, rolls back on an expected exception and honours unique constraints. Database cleanup must be explicit, using transaction-aware fixtures or isolated schemas so that one test cannot influence another.
For REST APIs, deploy the resource and use a client against the application’s real URL. This validates provider registration, JSON serialisation, validation annotations, exception mappers and authentication configuration. It can catch issues that a direct method call will never reveal, such as an incorrect @Path, a missing media type or a CDI bean that was omitted from the archive.
Asynchronous code requires extra discipline. A test that submits a message and immediately queries the database is inherently racy. Use a bounded polling utility, Awaitility or a message consumer that can observe completion, with a clear timeout and useful diagnostic output. Avoid unbounded sleeps: they slow every build while still failing unpredictably under load.
For test-data setup and filesystem-heavy verification, focused utilities can keep Arquillian classes readable. A small directory comparison utility can be useful when an export job produces nested files that need to be compared with a fixture tree. The Java EE test should verify the deployed service and invoke the utility at the boundary, rather than embedding file-comparison logic into every test method.
Keeping Builds Fast And Diagnostics Clear
Integration tests become expensive when every class starts a new server, loads a large EAR and recreates all infrastructure. Group related tests into a deployment where practical, reuse a managed container across a Maven profile, and reserve the heaviest scenarios for a dedicated integration-test phase. Maven Failsafe is generally a better fit than Surefire for tests that run after packaging.
Separate fast unit tests from container tests with naming conventions such as *Test and *IT. A developer can run the unit suite on every save, then invoke the Arquillian profile when changing deployment descriptors or container-facing code. CI should run both suites, with the integration phase publishing server logs, deployed archive details and database diagnostics as build artefacts.
Failure output matters as much as execution time. Log the container version, Java version, active profile and test database configuration without exposing credentials. When a remote container is involved, collect server logs around the failure window. This is especially valuable for teams coordinating across Sydney and Adelaide, where a test may fail in a shared environment while appearing healthy on a local machine.
Test Detective’s testing tutorials provide a useful place to connect Arquillian work with broader practices such as mocking, API testing, asynchronous verification and performance checks. The framework is most effective when it complements those techniques rather than replacing them.
Practical Recommendations For Sustainable Suites
A maintainable Arquillian suite has a clear purpose for every deployment. It should prove a container-level contract that a unit test cannot establish, while leaving detailed business-rule coverage to faster tests. The following practices keep the suite useful as the application grows:
- Keep each ShrinkWrap archive minimal and document why unusual libraries or descriptors are included.
- Pin the application-server, Java and database versions used by local development and CI.
- Use isolated schemas, unique test data or reliable cleanup to prevent order-dependent results.
- Give asynchronous tests bounded timeouts and failure messages that identify the missing event or state.
- Run container tests in a separate build phase and preserve server logs when a deployment fails.
Security deserves its own test boundary. If production uses an external identity provider, a remote Arquillian environment can verify roles and protected endpoints more accurately than an embedded server. For local feedback, a lightweight test identity service may be sufficient, provided the CI suite exercises the real authentication flow before release.
Performance testing should remain separate from ordinary integration checks. An Arquillian test can verify that an endpoint works with a transaction and a real database, but it is not a substitute for load generation, latency measurement or capacity analysis. Mixing hundreds of concurrent users into a functional suite makes failures difficult to classify and slows every pull request.
Making Arquillian Part Of Continuous Delivery
A sensible pipeline begins with unit tests, continues with packaging and Arquillian integration tests, and finishes with broader API, contract or performance checks. The container stage should run against the same major runtime family used in production. If the application is deployed to Payara in production, a WildFly-only local suite may provide false confidence even when both servers implement the same Jakarta EE specifications.
Build profiles can keep the workflow manageable. A local profile might use an embedded or managed container with H2, while the CI profile starts the production database version and enables security integration. A pre-release profile can deploy to a remote environment that resembles the target platform, including messaging, TLS termination and external configuration.
Australian teams also need to account for practical operating conditions: developers may work from home over variable NBN connections, while a shared CI service runs in a different region or availability zone. Keeping containers and service images cached in the relevant Australian cloud region can reduce delays, but reproducibility should never depend on an image that exists only on one workstation.
Arquillian is most valuable when it makes runtime assumptions visible. It exposes missing bean discovery, incorrect packaging, transaction mistakes and configuration drift before those defects reach a deployed environment. The essential principle is simple: use fast tests for logic, use Arquillian for container behaviour, and make every integration test prove one concrete production-facing assumption.