Integration tests for your Jakarta EE application are essential. Testing the application in a full setup will ensure all of your components can work together. The Testcontainers project provides a great approach to use Docker containers for such tests. With MicroShed Testing you get a convenient way to use Testcontainers for writing integration tests for your Jakarta EE application. This is also true for Java EE or standalone MicroProfile applications.
Learn how to use this framework with this blog post. I'm using a Jakarta EE 8, MicroProfile 3.0, Java 11 application running on Open Liberty for the example.
Project setup for MicroShed Testing
For creating this project I'm using the following Maven Archetype:
1 2 3 4 5 6 7 | mvn archetype:generate \ -DarchetypeGroupId=de.rieckpil.archetypes \ -DarchetypeArtifactId=jakartaee8 \ -DarchetypeVersion=1.0.0 \ -DgroupId=de.rieckpil.blog \ -DartifactId=review-microshed-testing \ -DinteractiveMode=false |
Besides the basic dependencies for Jakarta EE and Eclipse MicroProfile, we need some test dependencies also. As MicroShed Testing is using Testcontainers under the hood, we can add the official Testcontainers dependencies for starting a PostgreSQL and MockServer container.
Furthermore, we need JUnit 5 and the MicroShed Testing Open Liberty dependency itself. The Log4 1.2 SLF4J binding dependency is optional but improves the logging output of our tests.
As a result, the full pom.xml
looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.rieckpil.blog</groupId> <artifactId>review-microshed-testing</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <jakarta.jakartaee-api.version>8.0.0</jakarta.jakartaee-api.version> <microprofile.version>3.3</microprofile.version> <mockito-core.version>3.3.0</mockito-core.version> <junit-jupiter.version>5.6.0</junit-jupiter.version> <microshed-testing.version>0.9</microshed-testing.version> <testcontainers.version>1.14.2</testcontainers.version> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-api</artifactId> <version>${jakarta.jakartaee-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.eclipse.microprofile</groupId> <artifactId>microprofile</artifactId> <version>${microprofile.version}</version> <type>pom</type> <scope>provided</scope> </dependency> <dependency> <groupId>org.microshed</groupId> <artifactId>microshed-testing-liberty</artifactId> <version>${microshed-testing.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit-jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.29</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>${testcontainers.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mockserver</artifactId> <version>${testcontainers.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mock-server</groupId> <artifactId>mockserver-client-java</artifactId> <version>5.5.4</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>review-microshed-testing</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> </plugin> <plugin> <groupId>io.openliberty.tools</groupId> <artifactId>liberty-maven-plugin</artifactId> <version>3.1</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <!-- Plugin to run integration tests --> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M3</version> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> <configuration> <trimStackTrace>false</trimStackTrace> </configuration> </execution> <execution> <id>verify</id> <goals> <goal>verify</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> |
With MicroShed Testing, you can either provide your own Dockerfile
or in case of Open Liberty use the extension to have almost zero setup tasks. MicroShed Testing searches for a Dockerfile
in either the root folder of your project or within src/main/docker
.
In this example, I'm providing a custom Dockerfile:
1 2 3 4 5 6 7 | FROM openliberty/open-liberty:20.0.0.5-kernel-java11-openj9-ubi COPY --chown=1001:0 src/main/liberty/config/postgres /config/postgres COPY --chown=1001:0 src/main/liberty/config/server.xml /config COPY --chown=1001:0 target/review-microshed-testing.war /config/dropins/ RUN configure.sh |
For this application, I'm using the official Open Liberty extension from MicroShed Testing and therefore just have to configure the Open Liberty server within src/main/liberty
. This folder contains a custom server.xml
and the JDBC driver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?xml version="1.0" encoding="UTF-8"?> <server description="new server"> <featureManager> <feature>cdi-2.0</feature> <feature>jpa-2.2</feature> <feature>jaxrs-2.1</feature> <feature>mpConfig-1.4</feature> <feature>mpHealth-2.2</feature> <feature>mpRestClient-1.4</feature> </featureManager> <httpEndpoint id="defaultHttpEndpoint" httpPort="9080" httpsPort="9443"/> <dataSource id="DefaultDataSource"> <jdbcDriver libraryRef="postgresql-library"/> <properties.postgresql serverName="${POSTGRES_HOSTNAME}" portNumber="${POSTGRES_PORT}" databaseName="users" user="${POSTGRES_USERNAME}" password="${POSTGRES_PASSWORD}"/> </dataSource> <library id="postgresql-library"> <fileset dir="${server.config.dir}/postgres"/> </library> </server> |
Jakarta EE application walkthrough
As I don't want to provide a simple Hello World application for this example, I'm using an application with JPA & PostgreSQL and an external REST API as a dependency. This should mirror 80% of the basic applications out there.
The application contains two JAX-RS resources: SampleResource
and PersonResource
First, you can retrieve a MicroProfile Config property from the SampleResource
and a quote of the day. This quote of the day is fetched from a public REST API using MicroProfile RestClient:
1 2 3 4 5 6 7 8 | @RegisterRestClient(baseUri = "https://quotes.rest") public interface QuoteRestClient { @GET @Path("/qod") @Consumes(MediaType.APPLICATION_JSON) JsonObject getQuoteOfTheDay(); } |
Using this public REST API, I'll later demonstrate how to mock this call in your Jakarta EE integration test. The full resource looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @Path("sample") @Produces(MediaType.TEXT_PLAIN) public class SampleResource { @Inject @ConfigProperty(name = "message") private String message; @Inject @RestClient private QuoteRestClient quoteRestClient; @GET @Path("/message") public Response getMessage() { return Response.ok(message).build(); } @GET @Path("/quotes") public Response getQuotes() { var quoteOfTheDayPointer = Json.createPointer("/contents/quotes/0/quote"); var quoteOfTheDay = quoteOfTheDayPointer.getValue(quoteRestClient.getQuoteOfTheDay()).toString(); return Response.ok(quoteOfTheDay).build(); } } |
Second, the PersonResource
allows clients to create and read person resources. We'll store the persons in the PostgreSQL database using JPA:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | @Path("/persons") @ApplicationScoped @Transactional(TxType.REQUIRED) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class PersonResource { @PersistenceContext private EntityManager entityManager; @GET public List<Person> getAllPersons() { return entityManager.createQuery("SELECT p FROM Person p", Person.class).getResultList(); } @GET @Path("/{id}") public Person getPersonById(@PathParam("id") Long id) { var personById = entityManager.find(Person.class, id); if (personById == null) { throw new NotFoundException(); } return personById; } @POST public Response createNewPerson(@Context UriInfo uriInfo, @RequestBody Person personToStore) { entityManager.persist(personToStore); var headerLocation = uriInfo.getAbsolutePathBuilder() .path(personToStore.getId().toString()) .build(); return Response.created(headerLocation).build(); } } |
Integration test setup with MicroShed Testing
Next, we can set up the integration tests for our Jakarta EE (or Java EE or standalone MicroProfile) application. With MicroShed Testing, we can create a SharedContainerConfiguration
to share the Docker containers between integration tests. As our system requires a running application, a PostgreSQL database, and a remote system, I'm creating three containers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class SampleApplicationConfig implements SharedContainerConfiguration { @Container public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>() .withNetworkAliases("mypostgres") .withExposedPorts(5432) .withUsername("duke") .withPassword("duke42") .withDatabaseName("users"); @Container public static MockServerContainer mockServer = new MockServerContainer() .withNetworkAliases("mockserver"); @Container public static ApplicationContainer app = new ApplicationContainer() .withEnv("POSTGRES_HOSTNAME", "mypostgres") .withEnv("POSTGRES_PORT", "5432") .withEnv("POSTGRES_USERNAME", "duke") .withEnv("POSTGRES_PASSWORD", "duke42") .withEnv("message", "Hello World from MicroShed Testing") .withAppContextRoot("/") .withReadinessPath("/health/ready") .withMpRestClient(QuoteRestClient.class, "http://mockserver:" + MockServerContainer.PORT); } |
The PostgreSQLContainer
and MockServerContainer
are part of a Testcontainer dependency. ApplicationContainer
is now the first MicroShed Testing class we make use of. You can use this wrapper for plain Jakarta EE or Java EE applications.
We can set up the ApplicationContainer
with any environment variables, we want and provide MicroProfile Config properties in this way. Next, you can define the readiness path of your application. For this, we can make use of MicroProfile Health and specify the readiness path /health/ready
.
Furthermore, MicroShed Testing is capable to override the base URL for our MicroProfile RestClient. This allows us to use the MockServer for our integration tests.
Using the network alias our application is able to communicate with the MockServer and the PostgreSQL database within the Docker network.
Writing integration tests for Jakarta EE applications
Given this setup, we can finally start writing integration tests for our Jakarta EE application. MicroShed Testing can be enabled for a JUnit 5 test using @MicroShedTest
. With @SharedContainerConfig
you can reference the common system setup.
To test the SampleResource
class, I'm expecting to get MicroProfile Config property configured in the SharedContainerConfiguration
. Furthermore, the quotes endpoint should return the quote of the day properly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | @MicroShedTest @SharedContainerConfig(SampleApplicationConfig.class) public class SampleResourceIT { @RESTClient public static SampleResource sampleEndpoint; @Test public void shouldReturnSampleMessage() { assertEquals("Hello World from MicroShed Testing", sampleEndpoint.getMessage()); } @Test public void shouldReturnQuoteOfTheDay() { var resultQuote = Json.createObjectBuilder() .add("contents", Json.createObjectBuilder().add("quotes", Json.createArrayBuilder().add(Json.createObjectBuilder() .add("quote", "Do not worry if you have built your castles in the air. " + "They are where they should be. Now put the foundations under them.")))) .build(); new MockServerClient(mockServer.getContainerIpAddress(), mockServer.getServerPort()) .when(request("/qod")) .respond(response().withBody(resultQuote.toString(), com.google.common.net.MediaType.JSON_UTF_8)); var result = sampleEndpoint.getQuotes(); System.out.println("Quote of the day: " + result); assertNotNull(result); assertFalse(result.isEmpty()); } } |
Injecting the JAX-RS resource with @RESTClient
within the integration test allows us to make the HTTP call like using a JAX-RS client.
The integration test for PersonResource
is more advanced. I'm testing the flow of creating a new person and then querying for it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @MicroShedTest @SharedContainerConfig(SampleApplicationConfig.class) public class PersonResourceIT { @RESTClient public static PersonResource personsEndpoint; @Test public void shouldCreatePerson() { Person duke = new Person(); duke.setFirstName("duke"); duke.setLastName("jakarta"); Response result = personsEndpoint.createNewPerson(null, duke); assertEquals(Response.Status.CREATED.getStatusCode(), result.getStatus()); var createdUrl = result.getHeaderString("Location"); assertNotNull(createdUrl); var id = Long.valueOf(createdUrl.substring(createdUrl.lastIndexOf('/') + 1)); assertTrue(id > 0, "Generated ID should be greater than 0 but was: " + id); var newPerson = personsEndpoint.getPersonById(id); assertNotNull(newPerson); assertEquals("duke", newPerson.getFirstName()); assertEquals("jakarta", newPerson.getLastName()); } } |
Summary of MicroShed Testing for Jakarta EE integration tests
Even though the project is in its early stages, it provides excellent support for writing Jakarta EE integrations tests using Testcontainers. There are also dedicated dependencies available for Open Liberty and Payara. With them, it's, even more, simpler to use. You should give it a try and provide feedback to further evolve it.
There are also good examples available for different application setups.
For more information follow this guide on Open Liberty. Furthermore, visit the GitHub repository or the official project homepage. You can find this application also on GitHub.
Have fun writing Jakarta EE integration tests with MicroShed Testing,
Phil