Testing Java EE using Docker

Kai Winter
5 min readAug 8, 2016

--

This article has been 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 Hibernate layer with Docker. This article carries this idea a step further by deploying the complete application to a Dockerized Wildfly server. The example application needs a 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 it’s 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 27 and a MariaDB installation (kaiwinter/wildfly27-mariadb, available on GitHub). Wildfly is configured to use the 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:

  1. Unit test is started
  2. Arquillian runs our callback
  3. Our callback starts the Docker container, configures Arquillian, and inserts database schema
  4. Arquillian deploys the result of the @Deployment method
  5. Arquillian runs every @Test method
  6. Each @Test method inserts it’s test data by DbUnit, calls 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 on the configuration process.

The called listener starts the Docker container and blocks 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 container = new GenericContainer(
"ghcr.io/kaiwinter/wildfly27-mariadb:latest")
.withExposedPorts(8080, 9990, 3306);
container.start();
configureArquillianForRemoteWildfly(container, registry);
setupDb(container);
}

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 (schema) 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 it’s 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 on 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 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 have to set the DOCKER_HOST environment variable and can run integration tests.

Benefits:

  • Reduced setup time for new developers
  • Test running independent from local configuration
  • Always an empty database without side-effects
  • Implicitly tested data model and migration scripts
  • Multiple test classes can run concurrently, each runs in an own Docker container

Links

Disclaimer

Docker and Arquillian both seems 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 to use what I described here as a “always-to-be-used” solution. You should always prefer 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 which you can’t catch in unit test. Or for legacy code which has too many dependencies that are too complex to be mocked out.

--

--