How to Write Embedded Integration and E2E Tests for JakartaEE

Hüseyin Akdoğan
8 min readJun 30, 2020
from IWA website https://interwork.org
from IWA website https://interwork.org

The 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:

@RunWith(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);
}
@ArquillianResource //(2)
private URL baseURL;
@Deployment //(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;
}
@Test
@RunAsClient //(6)
@InSequence(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"));
}
@Test
@RunAsClient
@InSequence(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.

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();@Container //(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)
@Container
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);
@Container
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.

@MicroShedTest //(1)
@SharedContainerConfig(AppContainerConfig.class) //(2)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) //(3)
public class SystemTest
{
@RESTClient //(4)
public static OrderController orderController;
@Test
@Order(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"));
}
@Test
@Order(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());
}

@Test
@Order(3)
public void getAllOrderTest(){
final Response response = orderController.getAllOrders();
final List<OrderDemand> list = response.readEntity(List.class);
Assert.assertFalse(list.isEmpty());
}
@Test
@Order(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

Arquillian Guides

MicroShed Testing

--

--