Need for integration testing:
For all our projects we generally create two types of tests
- Unit Tests
- Integration Tests
Unit tests are comparatively simple as we validate a block of code which has some business logic. While doing so, we mock any external calls like external API, database read/updates, etc. This helps us to make sure the code which we have written is working correctly.
In addition to unit tests, we also write integration tests. These tests verify our code to make sure that our code is working correctly with the actual backend and database calls. This is very important as it helps us in discovering issues with external integrations like for example data serialization deserialization while calling an external API, object-relational mappings while performing database calls, etc and thus can’t be skipped.
Pain with end to end integration testing:
Integration tests were a pain for us. Integration tests require all non-prod environments to be up and running while tests are being performed. Non-prod availability was always a challenge and often resulted in failed integration tests. Another issue which we faced was incompatible version releases in different non-prod environments for different APIs resulting in more failures.
Due to all these issues, we made the integration test run optional. This resulted in escaped defects and worsened quality of code so we were forced to find some other solution. We tried many different approaches to make our integration run stable and finally went ahead with Docker.
How Docker helped us with our integration tests?
Docker came as a perfect solution for us to solve our integration woes. The idea was simple,
- Use the containerised artefacts of the backend service or the database which we were calling in our tests
- Start containers automatically before starting the integration tests. This was integrated in our maven builds thus no manual intervention was needed.
- Once all the container starts, execute your integration tests. Now these tests will run against our dockerised services rather than the original per-prod instances.
- Stop all the test containers automatically. This helps us to ensure there are no leftovers remaining once all the test are complete.
As said, we automate all this within our test suite using spotify docker client . It enabled us to run these tests on any machine with docker installed and rest was taken by the test setup and clean up phase.
Code sample
The complete code is available here. Just download the code and run the below command to execute the test. One clear benefit of this approach is that even though the integration test is calling an external service, still the test runs fine without additional configuration as it was using the dockerized version of the service which was started by the test itself.
mvn clean test
Let’s understand how we achieved this. We will be using an existing dockerized service which I have created in one of my earlier blog posts. The image for that service is available in my docker account and can be reused by anyone.
Add the below dependency to your code and start using the client.
<dependency>
<groupId>com.spotify</groupId>
<artifactId>docker-client</artifactId>
<version>8.11.6</version>
</dependency>
In the code sample ContainerManager class contains the methods to start and stop containers. We have to pass the image details to the startContainer method and it will start the container and will wait for the service to come up.
When ever we are starting docker containers we have to map the host ports with the container ports. The same thing is required here which will enable us to reach the service running inside the docker container
Map<String, List<PortBinding>> bindings = new HashMap<>();
Set<String> exposedPorts = new HashSet<>();
// Map the host and container ports
for(PortMapper mapper: image.getPorts()) {
List<PortBinding> hostPorts = new ArrayList<>();
hostPorts.add(PortBinding.create("0.0.0.0",
Integer.toString(mapper.getHostPort())));
bindings.put(Integer.toString(mapper.getContainerPort()), hostPorts);
exposedPorts.add(Integer.toString(mapper.getContainerPort()));
}
We can now start the container. Once the container is started we will poll the service to check if the service witin the container has also started. Only once the service is ready we will proceed with the tests. This is required as once the container is created, some service can take few seconds to start.
dockerClient.pull(image.getName());
dockerClient.startContainer(id);
// check if the application within the container has started
Status status = getContainerStatus(image.getName(), image.getHealthCheckEndPoint(), image.getTimeout());
if(status != Status.STARTED) {
throw new DockerException("Not able to communicate with the docker service");
}
return id;
For the complete configuration check the code in my github repository
We can now call this method from the method annotated with @BeforeClass Junit annotation. This will ensure that the container is started before the actuall tests are executed. Similarly we can call stopContainer method from the method annotated with @AfterClass
We also have to make sure that our test profile properties are using localhost for services endpoint as these containers are created on the machine where the tests are running and can be reached via the port mapped on the host.
Conclusion
We can see how containers for external services ensured that our integration tests are running fine. Optionally, we can create stub containers in case actual service containers are not fully ready and can start our integration tests without any dependency on actual service availability. One additional benefit which we can see is that now we can run our integration tests on any machine without any additional configuration.