Some weeks ago I have posted some thoughts on Testcontainers. When dealing with docker-compose↗ there is a way, that I prefer over using testcontainers.

divide tests in unit tests and integration tests

Personally I like the maven↗ style of testing (although there are some other things in maven that I really hate). It divides testing into several phases↗, that really make sense:

  • unit-tests: perform all tests that are easy to run because they have mocked dependencies.
  • pre-integration-test: setup a test fixture for the integration tests
  • integration-test: perform all the integration tests
  • post-integration-test: teardown the test fixture

I like to do testing like this with gradle↗ as well and Petri Kainulainen explains how to do it↗.

docker-compose gradle plugin

And for this scenario, I prefer Avast’s docker-compose gradle plugin↗.

build configuration

example docker-compose.yml

version: '3'

services:
  mysql:
    image: mysql:8.0.16
    ports:
      - '3306'

example build.gradle (extract)

task integrationTest(type: Test) {
    description = 'IntegrationTest'
    group = 'verification'

    useJUnitPlatform {
        includeTags 'integrationTest'
    }

    dependsOn compileTestJava, processTestResources, buildImage
}

dockerCompose {
    // see the logs of all services in build/docker-compose.log for debugging.
    captureContainersOutputToFile = 'build/docker-compose.log' 
}

dockerCompose.isRequiredBy(integrationTest)

check.dependsOn test, integrationTest

usage inside tests

The gradle plugin fills java’s system properties with the needed value to access the dependent services:

Inside the test you can obtain the mapped port using the name of the service and it’s internal port number:

String port = System.getProperty("mysql.tcp.3306");

local development

Sometimes I want to start the environment and leave it running and then I want to run one single test independently.

There are two ways to achieve this:

a) Use docker-compose.override.yml

  1. Put a docker-compose.override.yml next to the docker-compose file.
  2. Override the ports:

     version: '3'
        
     services:
       mysql:
         ports:
           - '3306:3306'
    
  3. add the docker-compose.override.yml to .gitignore so that you do not mess any other developer’s settings.
  4. provide the port from the docker-compose.override.yml as a default, wenn accessing the dependent service:

     int port = Integer.parseInt(System.getProperty("mysql.tcp.3306": "3306");
    

b) use docker inspect

    public class DockerInspect {
    
        DockerClient docker;
    
        public DockerInspect() {
            this.docker = DockerClientBuilder.getInstance().build();
        }
    
        public GetPorts getHostPort(String containerName) {
            InspectContainerResponse response = docker.inspectContainerCmd(containerName).exec();
            Map<ExposedPort, Ports.Binding[]> bindings = response.getNetworkSettings().getPorts().getBindings();
            return exposedPort -> {
                Ports.Binding[] portBindings = bindings.get(exposedPort);
                return asList(portBindings).stream().map(binding -> parseInt(binding.getHostPortSpec())).collect(Collectors.toList());
            };
        }
    
        public interface GetPorts {
            List<Integer> getPortOnHost(ExposedPort exposedPort);
        }
    }

Inside the test

    List<Integer> mysqlPorts = new DockerInspect().getHostPort("mysql").getPortOnHost(new ExposedPort(3306, InternetProtocol.TCP));

Comparison docker-compose testcontainers vs docker-compose gradle plugin

docker-compose testcontainers

  • Suitable when you have the view “I need a container in this test”.
  • Advantage: Perfectly cleans up the started containers after usage due to the Ryuk Container↗.

docker-compose gradle plugin

  • suitable when you have the view “I need a container in this test suite”
  • Advantage: see logs of all services in build/docker-compose.log for debugging with just one line of configuration.
  • Disadvantage: If your build job is interrupted/killed, there are sometimes some running containers left on the host. You need to setup a job on your ci server to kill them, e.g. in the night.

Any comments or suggestions? Leave an issue or a pull request!

Sergei Egorov↗ has written:

> Does some network voodoo that might interfere with your test setup.

What voodoo?

> see logs of all services in build/docker-compose.log for debugging

Can be done with TC

> Faster turnaround cycles

Reusable containers in TC

via Twitter↗

Thank you for your comments. I have updated the comparison section

  • Network: I have removed “Does some network voodoo that might interfere with your test setup.” I can not reproduce the issue anymore
  • Logs: I know that you can capture all logs with testcontainers, too. More information about that can be found on https://www.testcontainers.org/features/container_logs/ ↗. With the gradle-plugin it’s just dead simple with one line of configuration. I’ve changed it from “see logs of all services in build/docker-compose.log for debugging” to “see logs of all services in build/docker-compose.log for debugging with just one line of configuration.” to make it more clear.
  • Reusable: I haven’t seen Reusable Containers in Testcontainers before. Thank you for the link to your blog post about that↗. I’ve removed “Advantage: Faster turnaround cycles when developing locally because the containers can be left running.” from gradle-plugin advantages