How to Write Embedded Integration and E2E Tests for JakartaEE
In this article, I will try to introduce Arquillian and MicroShed test frameworks and explain how you can use them to meet the mentioned needs.
Join the DZone community and get the full member experience.
Join For FreeThe end-to-end testing
in enterprise applications is important as long as it covers the real use cases. Therefore, even although companies focus more on integration tests, end-to-end tests
are not neglected. Another common need, called System Integration Testing
, present-day is that perform to verify the interactions between the modules of a software system.
To be honest, outside the dev environment(for example in CI/CD pipelines), meet all these needs is a little harder at JakartaEE
(previously Java EE) compared to Spring Framework
. You must provide additional dependencies and configurations because by naturally there is no built-in embedded server support.
In this article, I will try to introduce Arquillian and MicroShed test frameworks and explain how you can use them to meet the mentioned needs. You can find the example that we will discuss in this article on GitHub.
Before Started
To embody the mentioned needs we will consider a microservices
example consisting of two services.
- Order Service
- Validation Service
The Order Service
is responsible for the persisting of the Order Demand
object from the client request body to the Database
after the credit card number inside the object is verified by the Validation Service
. The Validation Service
is responsible for validating the credit card number that passed by the Order Service
. It returns a flag about whether the number is validated or not with short info.
It can be noticed immediately, that the Validation Service
does not depend on any other service or system component due to its mission. Conversely, the Order Service
depends on the Validation Service
and it must persist the validated object to the Database
.
So we need an integration test
to evaluate the compliance of the Validation Service
with the specified functional requirement. As for the Order Service
, we can't just do integration testing
because it is dependent on other system components. We need an end-to-end test
to detect defects within all layers.
For these reasons, we will use Arquillian
for integration testing
and MicroShed Testing
for end-to-end testing
.
How to Write Embedded Tests with Arquillian?
Arquillian
is a testing framework for Java applications. Its main benefit is to handles the application server lifecycle for you. This facility provided by container adapters. Arquillian
has many container adapters. Because the adapter depends on the application server you want to use so the details of these adapters and how to configure these are out of scope this article. You can be found more details about Arquillian Container Adapters
here.
I used Liberty Managed Container Adapter with Liberty Maven plug-in in this example for managing Arquillian
dependencies and the setup to Arquillian Managed Container
. All these configuration details can be found on the repository.
Let's now focus on how to write tests with Arquillian
. We’ll develop test to verify the Validation Service
as an endpoint and the functions of the OrderController
class. The test looks like below:
xxxxxxxxxx
Arquillian.class) //(1) (
public class ValidationServiceIT
{
private final Client client;
private static final String PATH = "api/validation/{cardNumber}";
public ValidationServiceIT() {
this.client = ClientBuilder.newClient();
client.register(JsrJsonpProvider.class);
}
//(2)
private URL baseURL;
//(3)
public static WebArchive createDeployment() {
final WebArchive archive = ShrinkWrap.create(WebArchive.class,
"arquillian-validation-service.war") //(4)
.addClasses(ValidationController.class, ValidationService.class); //(5)
return archive;
}
//(6)
1) //(7) (
public void invalidCardNumberTest() {
final WebTarget webTarget = client.target(baseURL.toString()).path(PATH)
.resolveTemplate("cardNumber", "hello");
final Response response = webTarget.request().get();
final JsonObject result = response.readEntity(JsonObject.class);
Assert.assertFalse(result.getBoolean("approval"));
}
2) (
public void validCardNumberTest() {
final WebTarget webTarget = client.target(baseURL.toString()).path(PATH)
.resolveTemplate("cardNumber", "12345");
final Response response = webTarget.request().get();
final JsonObject result = response.readEntity(JsonObject.class);
Assert.assertTrue(result.getBoolean("approval"));
}
}
Let's examine how this works in detail by explaining all the marked pieces.
1) Firstly, with the @RunWith
annotation, we tell JUnit
to run the tests using Arquillian
, so JUnit
runs the tests with Arquillian
runner instead of the JUnit
runner.
2) To avoid hardcoding define of hostname, port number, and web archive information we use the @ArquillianResource
annotation and in this way retrieve the base URL(http://localhost:9090/arquillian-validation-service/, in our example).
3) We must define a method that returns a web archive to deploy our application onto the server we use(Open Liberty, in this example). The method should be annotated with @Deployment
annotation(it has an attribute of testable which enables the deployment to run the tests in the managed container, use in here is redundant because its default value is true) and have public
static
access modifiers with no arguments. The createDeployment
method fulfills these responsibilities.
4) Notice the arquillian-validation-service.war
name passed as the second parameter of the ShrinkWrap.create
method. You should provide a name if you don’t want a randomly generated web archive name.
5) To avoid injection failures, we must add the dependencies that our test needs because Arquillian
does not simply tap the entire classpath, unlike unit tests
. We can add what we need to use by addClass
, addClassess
, addPackages
, addAsResource
, addAsWebInfResourc
, etc methods.
6) We use @RunAsClient
annotation in invalidCardNumberTest
and validCardNumberTest
methods because we want to verify the Validation Service
as an endpoint. The annotation indicates that test cases are to be run on the client-side, so these tests are run against the managed container.
7) To guarantee the test sequence we use @InSequence
annotation.
In both invalidCardNumberTest
and validCardNumberTest
methods, we send card numbers by calls the baseURL + api/validation/{cardNumber} endpoint to verify card number. The test checks the validity of the numbers by the approval field of the object returned from the endpoint.
That is all. The test cases are ready to run. After execute mvn verfy
command, in the console output, you can see that the tests are passed.
How to Write E2E Tests With MicroShed Testing?
Another testing framework is MicroShed
for Java microservice applications. It uses Testcontainers under the hood hence your application runs inside a Docker
container. The main benefit of MicroShed
emerges at this point, it allows you to use your containerized application from outside the container, so it offers running true-to-production
tests.
Remember that, in our example, the Order service
was dependent on the Validation Service
and a Database
. This is exactly why we chose MicroShed
for this example because it enables us to access the Validation Service
and the Database
in the test environment by running these components in Docker
containers.
Minimum requirements for a MicroShed
test are a class with @MicroShedTest
annotation and an ApplicationContainer
object in which access modifiers are public
static
.
Let's examine our configuration and test classes.
xxxxxxxxxx
public class AppContainerConfig implements SharedContainerConfiguration // (1)
{
private static final String IMAGE_NAME = "hakdogan/validation-service:01";
private static final String SERVICE_NAME = "validation-service";
private static final String POSTGRES_NETWORK_ALIASES = "postgres";
private static final String POSTGRES_USER = "testUser";
private static final String POSTGRES_PASSWORD = "testPassword";
private static final String POSTGRES_DB = "orderDB";
private static final int VALIDATION_SERVICE_HTTP_PORT = 9080;
private static final int VALIDATION_SERVICE_HTTPS_PORT = 9443;
private static final int APPLICATION_SERVICE_HTTP_PORT = 9082;
private static final int APPLICATION_SERVICE_HTTPS_PORT = 9445;
private static final int POSTGRES_DEFAULT_PORT = 5432;
private static Network network = Network.newNetwork();
//(2)
public static GenericContainer validationService = new GenericContainer(IMAGE_NAME) //(3)
.withNetwork(network) //(4)
.withNetworkAliases(SERVICE_NAME) //(5)
.withEnv("HTTP_PORT", String.valueOf(VALIDATION_SERVICE_HTTP_PORT)) //(6)
.withEnv("HTTPS_PORT", String.valueOf(VALIDATION_SERVICE_HTTPS_PORT)) //(7)
.withExposedPorts(VALIDATION_SERVICE_HTTP_PORT) //(8)
.waitingFor(Wait.forListeningPort()); //(9)
public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>() //(10)
.withNetwork(network)
.withNetworkAliases(POSTGRES_NETWORK_ALIASES)
.withUsername(POSTGRES_USER)
.withPassword(POSTGRES_PASSWORD)
.withDatabaseName(POSTGRES_DB)
.withExposedPorts(POSTGRES_DEFAULT_PORT);
public static ApplicationContainer app = new ApplicationContainer() //(11)
.withNetwork(network)
.withEnv("POSTGRES_HOSTNAME", POSTGRES_NETWORK_ALIASES)
.withEnv("POSTGRES_PORT", String.valueOf(POSTGRES_DEFAULT_PORT))
.withEnv("POSTGRES_USER", POSTGRES_USER)
.withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD)
.withEnv("POSTGRES_DB", POSTGRES_DB)
.withEnv("VALIDATION_SERVICE_HOSTNAME", SERVICE_NAME)
.withEnv("HTTP_PORT", String.valueOf(APPLICATION_SERVICE_HTTP_PORT))
.withEnv("HTTPS_PORT", String.valueOf(APPLICATION_SERVICE_HTTPS_PORT))
.withExposedPorts(APPLICATION_SERVICE_HTTP_PORT)
.withAppContextRoot("/")
.waitingFor(Wait.forListeningPort())
.dependsOn(validationService, postgres); //(12)
}
Let's examine how this works in detail by explaining all the marked pieces.
1) To avoid started a new container for each class we implement SharedContainerConfiguration
. In this way, multiple test classes can share the same container instances.
2) The @Container
annotation is used to mark containers that should be managed by the Testcontainers
. Apart from Application Container
, we have defined two containers in this common configuration class for the components the Order service
is dependent on, these are Validation Service
and Postgresql
database. Notice that the annotation used in all.
3) The IMAGE_NAME
variable contains the Docker
image name of the Validation Service
that dockerized before.
4) We use the Network
object to create custom networks because we need to communicate between the Validation Service
and its dependent components. There fore, we place these components on the same network with the Application Container
.
5) The SERVICE_NAME
variable contains the alias name for accessing the Validation Service
in the Application Container
.
6-7) The HTTP_PORT
and HTTPS_PORT
environment variables tell the application server(Open Liberty, in this example) which ports to use. These are included before as placeholders in the server.xml
file.
8) We tell the Testcontainers
to expose the HTTP Port
of Validation Service
.
9) We tell the Testcontainers
to listen to the exposed port to check whether the container is ready for use as a wait strategy.
10) We define a Postgres
container with bootstrap parameters.
11) We define the application’s container. You can define it in several ways. Putting a Dockerfile
in your repository or passing image name to the constructor of ApplicationContainer
object as an argument or using vendor-specific adapters as a runtime option that will provide the default logic for building an application container. In this example, we use the last option by added microshed-testing-liberty
dependency in pom.xml
to automatically producing a testable container image. You can be found other runtime options here.
12) With that setting, we tell that the application’s container depends on the Validation Service
and Postgres
containers.
Let's examine our test class. It looks like below.
xxxxxxxxxx
//(1)
AppContainerConfig.class) //(2) (
MethodOrderer.OrderAnnotation.class) //(3) (
public class SystemTest
{
//(4)
public static OrderController orderController;
1) (
public void invalidCardNumberTest(){
final OrderDemand order = new OrderDemand(1, 1, "", "hello");
final Response response = orderController.saveOrder(order);
Assert.assertEquals(false, response.readEntity(JsonObject.class).getBoolean("approval"));
}
2) (
public void validCardNumberTest(){
final OrderDemand order = new OrderDemand(1, 1, "", "1234567");
final Response response = orderController.saveOrder(order);
Assert.assertNotNull(response.readEntity(OrderDemand.class).getId());
}
3) (
public void getAllOrderTest(){
final Response response = orderController.getAllOrders();
final List<OrderDemand> list = response.readEntity(List.class);
Assert.assertFalse(list.isEmpty());
}
4) (
public void getAllOrderByProductIdTest(){
final Response response = orderController.getAllOrdersByProductId(1);
final List<OrderDemand> list = response.readEntity(List.class);
Assert.assertFalse(list.isEmpty());
}
}
Let's examine how this works in detail by explaining all the marked pieces.
1) We indicate with the annotation that the test class uses MicroShed Testing
.
2) We define our shared configuration to use by the test class.
3) MicroShed Testing
runs with JUnit Jupiter
. We define ordering method with the TestMethodOrder
annotation to guarantee the test sequence.
4) The annotation identifies an injection point for a JAX-RS REST Client
. Any method calls to the injected object will be translated to an equivalent REST request
via HTTP
. The annotated field must be public
static
and non-final
.
In both invalidCardNumberTest
and validCardNumberTest
methods we call api/order/save endpoint via orderController
reference that annotated with @RESTClient
. This endpoint triggers the validation process by calling the Validation Service
then persists the object transmitted to it to the Database
with request body if the card number is valid.
In both getAllOrderTest
and getAllOrderByProductIdTest
methods, after the test case which contains the valid credit card number, we check whether the object persisted or not in the Database
.
In this way, we have done an end-to-end
test. After execute mvn verfy
command, in the console output, you can see that the tests are passed.
Conclusion
Embedded tests are almost indispensable in today's microservices and multitier architectures world, especially in the CI/CD pipelines/environments. Test frameworks like Arquillian
and MicroShed
(of course also Testcontainers
) responds to this important need for JakartaEE
applications. If we compare the two frameworks, we can tell quickly go through some of the differences.
Arquillian
allows you to inject all objects managed by the CDI container
into your test class. MicroShed
, on the other hand, only allows JAX-RS resources classes
injection to the test class. Arquillian
has ceremony and verbosity for XML configuration
. MicroShed
does not require XML configuration
. Arquillian
does not give you an option to test the system components that your application depends on, but MicroShed
offers. We can say that Arquillian
has a strong community, but we cannot say the same for MicroShed
.
References
Opinions expressed by DZone contributors are their own.
Trending
-
Writing a Vector Database in a Week in Rust
-
How To Approach Java, Databases, and SQL [Video]
-
Structured Logging
-
Microservices With Apache Camel and Quarkus (Part 2)
Comments