Unit Integration Testing With Testcontainers Docker Compose

Is your test dependent on multiple other applications and do you want to create an integration test using Testcontainers? Then the Testcontainers Docker Compose Module is the solution. In this blog, you will learn how convenient it is to create an integration test using multiple Testcontainers. Enjoy!

Introduction

Using Testcontainers is a very convenient way to write a unit integration test. Most of the time, you will use it in order to test the integration with a database, a message bus, etc. But what if your application interacts with an application that consists of multiple containers? How can you add these containers to your unit integration test? The answer is quite simple: you should use the Docker Compose Module.

In the remainder of this blog, you will create a test that depends on another application that consists of two containers. Afterward, you will alter the unit tests using the Docker Compose Module.

The sources used in this blog are available on GitHub.

Prerequisites

Prerequisites for reading this blog are:

Application To Integrate With

You need an application to integrate with consisting out of more than one container. One of those applications is FROST-Server. FROST-Server is a server implementation of the OGC SensorThings API. The OGC SensorThings API is an OGC (Open Geospatial Consortium) standard specification for providing an open and unified way to interconnect IoT devices, data, and applications over the Web. The SensorThings API is an open standard applies an easy-to-use REST-like style. The result is to provide a uniform way to expose the full potential of the Internet of Things.

For this blog, you do not need to know what FROST-Server really is. But, the application can be started by means of a Docker Compose file and it consists out of two containers: one for the server and one for the database. So, this is interesting, because when you want to create an application which needs to interact with FROST-Server, you also want to create unit integration tests. FROST-Server provides a Rest API with CRUD operations for the SensorThings API objects.

The Docker Compose file consists of the FROST-Server itself and a PostgreSQL database. The PostgreSQL database has a health check and FROST-Server is dependent on the successful startup of the database.

YAML
 
services:
  web:
    image: fraunhoferiosb/frost-server:2.3.2
    environment:
      - serviceRootUrl=http://localhost:8080/FROST-Server
      - plugins_multiDatastream.enable=false
      - http_cors_enable=true
      - http_cors_allowed_origins=*
      - persistence_db_driver=org.postgresql.Driver
      - persistence_db_url=jdbc:postgresql://database:5432/sensorthings
      - persistence_db_username=sensorthings
      - persistence_db_password=ChangeMe
      - persistence_autoUpdateDatabase=true
    ports:
      - 8080:8080
      - 1883:1883
    depends_on:
      database:
        condition: service_healthy
 
  database:
    image: postgis/postgis:14-3.2-alpine
    environment:
      - POSTGRES_DB=sensorthings
      - POSTGRES_USER=sensorthings
      - POSTGRES_PASSWORD=ChangeMe
    volumes:
      - postgis_volume:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d sensorthings -U sensorthings "]
      interval: 10s
      timeout: 5s
      retries: 5
 
volumes:
  postgis_volume:


Start the Docker containers from the root of the Git repository.

Shell
 
$ docker compose up


Now that the application is running, you can write a unit integration test for this.

The test will:

  1. Send a POST request in order to create a SensorThings API Thing object.
  2. Verify the successful creation of the Thing object (HTTP status 201 Created is received).
  3. Send a GET request in order to retrieve the contents of the Thing object.
  4. Verify the body of the GET request.
Java
 
class ManualStartTest {
 
    @Test
    void testApp() throws Exception {
 
        try (HttpClient client = HttpClient.newHttpClient()) {
            String baseUrl = "http://localhost:8080/FROST-Server/v1.1/Things";
            URI uri = URI.create(baseUrl);
 
            // JSON payload to be sent in the request body
            String postPayload = """
                {
                  "name" : "Kitchen",
                  "description" : "The Kitchen in my house",
                  "properties" : {
                    "oven" : true,
                    "heatingPlates" : 4
                  }
                }
            """;
 
            // Post the request
            HttpRequest post = HttpRequest.newBuilder()
                    .uri(uri)
                    .method("POST", HttpRequest.BodyPublishers.ofString(postPayload))
                    .header("Content-Type", "application/json")
                    .build();
            HttpResponse<String> response = client.send(post, HttpResponse.BodyHandlers.ofString());
            assertEquals(201, response.statusCode());
 
            // Get the Thing
            URI getUri = URI.create(baseUrl + "(1)");
            HttpRequest get = HttpRequest.newBuilder()
                    .uri(getUri)
                    .GET()
                    .header("Content-Type", "application/json")
                    .build();
            String expected = "{\"@iot.selfLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)\",\"@iot.id\":1,\"name\":\"Kitchen\",\"description\":\"The Kitchen in my house\",\"properties\":{\"heatingPlates\":4,\"oven\":true},\"HistoricalLocations@iot.navigationLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)/HistoricalLocations\",\"Locations@iot.navigationLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)/Locations\",\"Datastreams@iot.navigationLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)/Datastreams\"}";
            response = client.send(get, HttpResponse.BodyHandlers.ofString());
            assertEquals(expected, response.body());
        }
    }
 
}


The test executes successfully, but you had to ensure that the FROST-Server was started manually. And this is not what you want: you want to use Testcontainers for it.

Stop the running containers with CTRL+C.

Use Docker Compose Module

In order to make use of the Docker Compose Module, you need to make some minor changes to the unit integration test.

In order to be able to use Testcontainers, you add the following dependencies to your pom.

XML
 
<dependencyManagement>
  <dependencies>
    ...
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>testcontainers-bom</artifactId>
      <version>1.19.8</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>
 
<dependencies>
 
  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
  </dependency>
 
  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
  </dependency>
 
  ...
 
</dependencies>


You put the compose.yaml file in the test resources directory: src/test/resources/compose.yaml.

You annotate the unit test with the Testcontainers annotation.

Java
 
@Testcontainers
class TestcontainersComposeTest {
...
}


You create the DockerComposeContainer environment:

  1. Create the environment with a reference to the Docker Compose file.
  2. By means of withExposedService, you indicate which services you want to add to your test. In this case the database service and the web service as defined in the Docker Compose file.
  3. The web service is dependent on the database service as defined in the Docker Compose file, but you also need to wait for the successful startup of the web service before running the tests. Therefore, you define a WaitStrategy for the web service. The easiest way to do so is to wait for a specific log message.
  4. You annotate the environment with the Container annotation so that Testcontainers will manage the lifecycle of this environment.
Java
 
@Container
private static final DockerComposeContainer environment =
        new DockerComposeContainer(new File("src/test/resources/compose.yaml"))
                .withExposedService("database", 5432)
                .withExposedService("web", 
                        8080, 
                        Wait.forLogMessage(".*org.apache.catalina.startup.Catalina.start Server startup in.*\\n", 
                                1));


As Testcontainers will run the containers on a random port, you also need to retrieve the host URL and port when running the test. You can do so by invoking the getServiceHost and getServicePort methods.

Java
 
String frostWebUrl = environment.getServiceHost("web", 8080) + ":" + environment.getServicePort("web", 8080);
String baseUrl = "http://" + frostWebUrl + "/FROST-Server/v1.1/Things";
URI uri = URI.create(baseUrl);


The complete unit test is the following:

Java
 
@Testcontainers
class TestcontainersComposeTest {
 
    @Container
    private static final DockerComposeContainer environment =
            new DockerComposeContainer(new File("src/test/resources/compose.yaml"))
                    .withExposedService("database", 5432)
                    .withExposedService("web",
                            8080,
                            Wait.forLogMessage(".*org.apache.catalina.startup.Catalina.start Server startup in.*\\n",
                                    1));
 
    @Test
    void testApp() throws Exception {
 
        try (HttpClient client = HttpClient.newHttpClient()) {
            String frostWebUrl = environment.getServiceHost("web", 8080) + ":" + environment.getServicePort("web", 8080);
            String baseUrl = "http://" + frostWebUrl + "/FROST-Server/v1.1/Things";
            URI uri = URI.create(baseUrl);
 
            // JSON payload to be sent in the request body
            String postPayload = """
                {
                  "name" : "Kitchen",
                  "description" : "The Kitchen in my house",
                  "properties" : {
                    "oven" : true,
                    "heatingPlates" : 4
                  }
                }
            """;
 
            // Post the request
            HttpRequest post = HttpRequest.newBuilder()
                    .uri(uri)
                    .method("POST", HttpRequest.BodyPublishers.ofString(postPayload))
                    .header("Content-Type", "application/json")
                    .build();
            HttpResponse<String> response = client.send(post, HttpResponse.BodyHandlers.ofString());
            assertEquals(201, response.statusCode());
 
            // Get the Thing
            URI getUri = URI.create(baseUrl + "(1)");
            HttpRequest get = HttpRequest.newBuilder()
                    .uri(getUri)
                    .GET()
                    .header("Content-Type", "application/json")
                    .build();
            String expected = "{\"@iot.selfLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)\",\"@iot.id\":1,\"name\":\"Kitchen\",\"description\":\"The Kitchen in my house\",\"properties\":{\"heatingPlates\":4,\"oven\":true},\"HistoricalLocations@iot.navigationLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)/HistoricalLocations\",\"Locations@iot.navigationLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)/Locations\",\"Datastreams@iot.navigationLink\":\"http://localhost:8080/FROST-Server/v1.1/Things(1)/Datastreams\"}";
            response = client.send(get, HttpResponse.BodyHandlers.ofString());
            assertEquals(expected, response.body());
        }
    }
 
}


Run the test and you will have successfully executed a unit integration test using multiple Testcontainers!

Conclusion

Using Testcontainers for running an integration test against one container is pretty straightforward. But when you are in need of multiple Testcontainers for your unit integration test, then you can use the Docker Compose Module for that.

 

 

 

 

Top