DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Micronaut With Relation Database and...Tests
  • Maven Plugin Testing - in a Modern Way - Part I
  • Write Integration Tests on MongoDB With .NET Core and Docker
  • Unit Testing Large Codebases: Principles, Practices, and C++ Examples

Trending

  • Tired of Spring Overhead? Try Dropwizard for Your Next Java Microservice
  • Introduction to Retrieval Augmented Generation (RAG)
  • Exploring Intercooler.js: Simplify AJAX With HTML Attributes
  • Security by Design: Building Full-Stack Applications With DevSecOps
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Unit Integration Testing With Testcontainers Docker Compose

Unit Integration Testing With Testcontainers Docker Compose

Is your test dependent on multiple other applications? Do you want to create an integration test using Testcontainers? Learn about the Docker Compose Module.

By 
Gunter Rotsaert user avatar
Gunter Rotsaert
DZone Core CORE ·
Oct. 02, 24 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
5.2K Views

Join the DZone community and get the full member experience.

Join For Free

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:

  • Basic Java knowledge, Java 21 is used
  • Basic Testcontainers knowledge
  • Basic knowledge of Docker Compose

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.

Docker (software) integration test unit test Data Types Integration

Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Micronaut With Relation Database and...Tests
  • Maven Plugin Testing - in a Modern Way - Part I
  • Write Integration Tests on MongoDB With .NET Core and Docker
  • Unit Testing Large Codebases: Principles, Practices, and C++ Examples

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!