A few weeks back I decided to write a blog post on how to efficiently test a Java application that uses the OpenDJ SDK to connect to an LDAP store (read post here). Since the scope was so big I had to break it down into two smaller posts. In this second part I will walk you through a sample maven-based application written in Java that uses Docker for integration testing.

The following diagram illustrates the application build process:

app-sdlc

The sample app follows the standard Maven Build Life Cycle but it adds a couple of phases to it so that we can build, run and destroy a Docker container before the Integration Test Phase.

About the Sample App

You can download the sample application from my Github repo.

The application uses the OpenDJ Java SDK to retrieve User objects from a Directory Server and perform operations on them (create, update, and delete). The code is pretty straight forward so feel free to explore it if you want to understand how it works. However, in this post I am just going to focus on testing the Person Data Access Object (PersonDAO.java). The interface is self-explanatory:

public interface PersonDAO {
    
    void addPerson(Person person);
    public Person getPerson(String username);
    public void updatePerson(Person person);
    public void deletePerson(String username);

}

Unit vs Integration Testing

The sample app uses JUnit for both unit and integration testing. To distinguish between these two types of test classes we will use JUnit categories. This will also allow us to link each test to a different build phase.

The IntegrationTest interface is an empty interface that will be used as a marker class:

public interface IntegrationTest {
}

Use the following annotation to add a test class to the IntegrationTest category:

@Category(IntegrationTest.class)
public class MyIntegrationTestCase {
    //Your tests here
}

Now that you know how to mark your tests we need to tell the maven-surefire-plugin to skip all the annotated classes during the test phase so that only unit tests get executed:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19</version>
    <!-- Exclude integration tests -->
    <configuration>
        <excludedGroups>com.groman.opendj.dao.IntegrationTest</excludedGroups>
    </configuration>
</plugin>

Similarly we need to configure the maven-failsafe-plugin to only execute annotated classes during the integration-test phase:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.19</version>
    <!-- Only run integration tests -->
    <configuration>
        <groups>com.groman.opendj.dao.IntegrationTest</groups>
    </configuration>
    <executions>
        <execution>
	    <goals>
		<goal>integration-test</goal>
	    </goals>
	    <configuration>
		<includes>
		    <include>**/*.class</include>
		</includes>
	    </configuration>
	</execution>
    </executions>
</plugin>

The sample application only has one unit test  (PersonDAOUnitTestCase) and one integration test (PersonDAOIntegrationTestCase). Both classes extend from a base class called AbstractPersonDAOTestCase that implements most of the logic. The only difference between these two classes is how they initialize the LDAP connection. While PersonDAOUnitTestCase uses an in-memory backend, PersonDAOIntegrationTestCase establishes a "real" connection to an external LDAP server (in this case it will connect to a Docker container).

Sample Unit Test

PersonDAOUnitTestCase makes use of the Memory Backend provided by the OpenDJ SDK. In other words, it won't actually connect to an external LDAP server. In Part 1 I talked about this feature extensively.

Sample Integration Test

For integration testing the idea is to spin up a Docker container running OpenDJ, execute the integration tests, and stop the container when the tests are done. The question is, how can we integrate this process into the maven build lifecycle? Well, we could write our own custom plugin...Luckily somebody has done all the hard work for us. This docker-maven-plugin does exactly what we need, and it works great. Here is how it works.

First we need to find a place to store the Dokerfile and all it's dependencies such as LDIF and schema files. I will use the same artifacts from one of my previous post. You can find these files in the following location:

project screenshot

Time to add the docker-maven-plugin to our pom.xml. The plugin will get invoked three times during the build process:

  • build-images: executes the Dockerfile and build the image.
<execution>
    <id>package</id>
    <goals>
        <goal>build-images</goal>
    </goals>
    <configuration>
	<images>
	    <image>
		<id>opendj</id>
		<dockerFile>${project.basedir}/src/test/resources/opendj/Dockerfile</dockerFile>
	        <keep>true</keep>
	        <nameAndTag>groman/opendj-test:1.0</nameAndTag>
	        <artifacts>
		    <artifact>
                        <file>${project.basedir}/src/test/resources/opendj/opendj.zip</file>
                    </artifact>
		    <artifact>
		        <file>${project.basedir}/src/test/resources/opendj/99-custom-schema.ldif</file>
		    </artifact>
		    <artifact>
		         <file>${project.basedir}/src/test/resources/opendj/custom-dit.ldif</file>
		    </artifact>
		</artifacts>
	    </image>
	</images>
     </configuration>
</execution>
  • start-containers: starts the container.
<execution>
    <id>start</id>
    <goals>
	<goal>start-containers</goal>
    </goals>
    <configuration>
        <forceCleanup>false</forceCleanup>
	<containers>
	    <container>
	        <id>opendj</id>
		<image>groman/opendj-test:1.0</image>
		<waitForStartup>The Directory Server has started successfully</waitForStartup>
	    </container>
	</containers>
    </configuration>
</execution>
  • stop-containers: stops and destroys the container.
<execution>
    <id>stop</id>
    <goals>
	<goal>stop-containers</goal>
    </goals>
</execution>

If you are familiar with Docker you'll know that by default it uses ephemeral ports, which means we cannot assume that the port values will always be the same. But no worries, the plugin sets all this information in a bunch of maven properties. This information will be printed out during the build:

[INFO] Starting container 'opendj'..
[INFO] Setting property 'docker.containers.opendj.ports.1389/tcp.host' to '192.168.99.100'
[INFO] Setting property 'docker.containers.opendj.ports.4444/tcp.host' to '192.168.99.100'
[INFO] Setting property 'docker.containers.opendj.ports.1389/tcp.port' to '32785'
[INFO] Setting property 'docker.containers.opendj.ports.1636/tcp.host' to '192.168.99.100'
[INFO] Setting property 'docker.containers.opendj.ports.4444/tcp.port' to '32783'
[INFO] Setting property 'docker.containers.opendj.ports.1636/tcp.port' to '32784'

So all we need to do is grab those maven properties and turn them into Java System properties. You can do this by simply adding the following lines to the pom.xml:

<systemPropertyVariables>
    <opendj.hostname>${docker.containers.opendj.ports.1389/tcp.host}</opendj.hostname>
    <opendj.ldap.port>${docker.containers.opendj.ports.1389/tcp.port}</opendj.ldap.port>
    <opendj.bindDN>cn=directory manager</opendj.bindDN>
    <opendj.bindPassword>password</opendj.bindPassword>
</systemPropertyVariables>

Then, your Java app can retrieve the values using System.getProperty():

String hostname = System.getProperty("opendj.hostname");
int port = Integer.parseInt(System.getProperty("opendj.ldap.port"));
String bindDN = System.getProperty("opendj.bindDN");
String bindPassword = System.getProperty("opendj.bindPassword");

Let's Run It!

I know you can't wait to see all this in action so go ahead and run the following maven command to execute the unit tests:

mvn test

........

[INFO] --- maven-surefire-plugin:2.19:test (default-test) @ opendj-sample ---

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.groman.opendj.dao.PersonDAOUnitTestCase
[main] DEBUG com.groman.opendj.service.LdapService - Creating service
[main] DEBUG com.groman.opendj.service.LdapService - Starting service
[main] DEBUG com.groman.opendj.dao.MemoryConnectionManager - Instantiating In-Memory Connection Factory
[main] DEBUG com.groman.opendj.service.LdapService - Stopping service
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.393 sec - in com.groman.opendj.dao.PersonDAOUnitTestCase

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0


........

Now invoke the 'verify' phase to execute the Integration Tests as well:

mvn verify

........

[INFO] --- maven-failsafe-plugin:2.19:integration-test (default) @ opendj-sample ---

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.groman.opendj.dao.PersonDAOIntegrationTestCase [main] DEBUG com.groman.opendj.service.LdapService - Creating service [main] DEBUG com.groman.opendj.service.LdapService - Starting service [main] DEBUG com.groman.opendj.dao.SimpleConnectionManager - Setting up Simple LDAP Connection factory: LDAPConnectionFactory(192.168.99.100:32788) [main] DEBUG com.groman.opendj.service.LdapService - Stopping service Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.969 sec - in com.groman.opendj.dao.PersonDAOIntegrationTestCase Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 ........

And that concludes this series of blog posts. Hope you find this useful!