Testing Java EE Using Docker
Need to run an integration test for your JavaEE application? Well, here are some tools that can help you out and a roadmap of how to use them.
Join the DZone community and get the full member experience.
Join For FreeThis article was updated in March 2023 for Java 17, Wildfly 27, Hibernate 6, JUnit 5, and Testcontainers 1.17.6.
In a previous post, I described how to test the persistence layer against a Dockerized database. This article carries this idea a step further by deploying the complete application to a Dockerized Wildfly server. The example application needs MariaDB, which is installed in the same Docker image as the Wildfly server.
The big benefit is that those integration tests feel like unit tests because they are completely self-contained. They can be run from the IDE as well as on the integration server without having a local application server or database server and without all those local machine-wise configurations. The only precondition is an available Docker host, either local or remote. Test classes can even run in parallel because each one gets its own environment.
What Do We Need
- A Docker host, local or remote.
- A Docker image with the application server and database server installed.
- Arquillian to deploy to the Dockerized server.
- An Arquillian callback to start the Docker container and configure Arquillian to deploy to it.
- Testcontainers library to manage the Docker container.
Overview
A custom Docker image is used, which contains a Wildfly 10 and a MariaDB installation (kaiwinter/wildfly27-mariadb
, available on GitHub). Wildfly is configured to use MariaDB, and a management user is set up to let Arquillian deploy to this server. The deployment is done by wildfly-arquillian-container-remote
.
These are the rough steps when a test is started:
- Unit test is started.
- Arquillian runs our callback.
- Our callback starts the Docker container, configures Arquillian, and inserts the database schema.
- Arquillian deploys the result of the
@Deployment
method. - Arquillian runs every
@Test
method. - Each
@Test
method inserts its test data by DbUnit, calls the test code, and verifies the result.
The use of Docker is transparent for the test. You can use any existing Arquillian test without changes.
Setup
The tricky part is the dynamic configuration of Arquillian to let it deploy to the Dockerized Wildfly. This is necessary because the server ports are exposed on random ports to the outside of the Docker container to allow the parallel use of multiple instances of the same image. So, after the Docker container is running, we need to tell Arquillian the exposed management port of Wildfly.
Arquillian is configured by the file arquillian.xml
and it cannot be changed by an API dynamically. But we can register a org.jboss.arquillian.core.spi.LoadableExtension
service (via META-INF/services
), which can register a listener in the configuration process.
The called listener starts the Docker container and blocks it until it runs. Then, it configures Arquillian and inserts the database schema (see WildflyMariaDBDockerExtension
):
/**
* Method which observes {@link ContainerRegistry}. Gets called by Arquillian at startup time.
*/
public void registerInstance(@Observes ContainerRegistry registry, ServiceLoader serviceLoader) {
GenericContainer dockerContainer = new GenericContainer("ghcr.io/kaiwinter/wildfly27-mariadb:latest")
.withExposedPorts(8080, 9990, 3306);
dockerContainer.start();
configureArquillianForRemoteWildfly(dockerContainer, registry);
setupDb(dockerContainer);
}
Test Example
The test looks like a common Arquillian test:
@ExtendWith(ArquillianExtension.class)
class UserServiceTest {
@Inject
private UserService userService;
@PersistenceContext
private EntityManager entityManager;
@Deployment
public static EnterpriseArchive createDeployment() {
// ... EAR creation
}
@Test
void testSumOfLogins() {
DockerDatabaseTestUtil.insertDbUnitTestdata(entityManager,
getClass().getResourceAsStream("/testdata.xml"));
int sumOfLogins = userService.calculateSumOfLogins();
assertEquals(9, sumOfLogins);
}
}
This is the complete example: UserServiceTest.
Inserting Testdata
There are several options to insert the data model and test data into the database:
- .sql files (ScriptUtils class of testcontainers).
- Single SQL statements.
- DBUnit: Truncates all tables and inserts test data from XML files.
- Flyway: When you need DB migrations.
The database model needs to be inserted only once. So, the best place to do this is the Arquillian listener class, where the Docker container is started. This can be done by a plain JDBC connection (example) or by a database migration library like Flyway (example).
Then, each unit test in the test class can insert its individual test data by DBUnit or from .sql files. The advantage of DBUnit is that it automatically removes existing data from the database. When you go with .sql files, you may want to use transactions to separate the test cases.
Debugging
It is possible to debug the running application on the Dockerized Wildfly server. The Docker image exposes the port 8787 to let a debugger attach.
Debugging With Eclipse
After setting a breakpoint in the server or test code (also, the test code runs on the server), create a Debug Configuration for a Remote Java Application. Use the IP of the Docker Host for the connection. As mentioned earlier, the container ports are exposed to dynamic ports. This means that after the container is started, you have to check to which port the debugging port 8787 was mapped and have to put it in your Debug Configuration. This is a bit inconvenient.
Alternative to Testcontainers
An alternative to the combination of Arquillian and testcontainers might be Arquillian Cube, which allows us to put a Docker Compose script in the arquillian.xml
file. But when I was working on this (end of 2015), the project was a bit undocumented, and I couldn't get it running. Today, the documentation looks much better.
Summary
In the past, every developer got to have a local application server to deploy to and a local database server. This had to be configured on each developer's machine. The same had to be done for the integration server once. This setup time can be reduced to almost zero. When having a central Docker host, a developer just has to set the DOCKER_HOST
environment variable and can run integration tests.
Benefits
- Reduced setup time for new developers.
- Test running independently from local configuration.
- Always an empty database without side effects.
- Implicitly tested data model and migration scripts.
- Multiple test classes can run concurrently, and each runs in its own Docker container.
Links
Disclaimer
Docker and Arquillian both seem to be very polarizing. I try to keep a pragmatic sight on both topics, but I also try to avoid both.
I wouldn't recommend using what I described here as an "always-to-be-used" solution. You should always prefer a small and fast unit test over an integration test with Arquillian. Sebastian Daschner wrote about it here: Testing Java EE (or Why Integration Tests Are Overrated). I also recommend writing integration tests only for things that you can't catch in unit tests or for legacy code that has too many dependencies that are too complex to be mocked out.
Published at DZone with permission of Kai Winter. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments