Building true-to-production integration tests with Testcontainers

duration 20 minutes
New

Prerequisites:

Learn how to test your microservices with multiple containers by using Testcontainers and JUnit.

What you’ll learn

You’ll learn how to write true-to-production integration tests for Java microservices by using Testcontainers and JUnit. You’ll learn to set up and configure multiple containers, including the Open Liberty Docker container, to simulate a production-like environment for your tests.

Sometimes tests might pass in development and testing environments, but fail in production because of the differences in how the application operates across these environments. Fortunately, you can minimize these differences by testing your application with the same Docker containers you use in production. This approach helps to ensure parity across the development, testing, and production environments, enhancing quality and test reliability.

What is Testcontainers?

Testcontainers is an open source library that provides containers as a resource at test time, creating consistent and portable testing environments. This is especially useful for applications that have external resource dependencies such as databases, message queues, or web services. By encapsulating these dependencies in containers, Testcontainers simplifies the configuration process and ensures a uniform testing setup that closely mirrors production environments.

The microservice that you’ll be working with is called inventory. The inventory microservice persists data into a PostgreSQL database and supports create, retrieve, update, and delete (CRUD) operations on the database records. You’ll write integration tests for the application by using Testcontainers to run it in Docker containers.

Inventory microservice

Additional prerequisites

Before you begin, Docker needs to be installed. For installation instructions, see the official Docker documentation. You’ll test the application in Docker containers.

Make sure to start your Docker daemon before you proceed.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-testcontainers.git
cd guide-testcontainers

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Try what you’ll build

The finish directory in the root of this guide contains the finished application. Give it a try before you proceed.

To try out the test, first go to the finish directory and run the following Maven goal that builds the application, starts the containers, runs the tests, and then stops the containers:

cd finish
mvn verify
export TESTCONTAINERS_RYUK_DISABLED=true
cd finish
mvn verify

You see the following output:

 -------------------------------------------------------
  T E S T S
 -------------------------------------------------------
 Running it.io.openliberty.guides.inventory.SystemResourceIT
 ...
 Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.118 s - in it.io.openliberty.guides.inventory.SystemResourceIT

 Results:

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

Writing integration tests using Testcontainers

Use Testcontainers to write integration tests that run in any environment with minimal setup using containers.

Navigate to the postgres directory.

postgres/Dockerfile

 1FROM postgres:16.2
 2
 3# set env variable for password to docker
 4ENV POSTGRES_PASSWORD adminpwd
 5# create database called database
 6ENV POSTGRES_USER admin
 7# Create the default database for PostgreSQL
 8ENV POSTGRES_DB admindb
 9
10# Use dumpfile to create table schema and populate it with data
11COPY schema.sql /docker-entrypoint-initdb.d/1-schema.sql
12
13EXPOSE 5432

This guide uses Docker to run an instance of the PostgreSQL database for a fast installation and setup. A Dockerfile file is provided for you. Run the following command to use the Dockerfile to build the image:

docker build -t postgres-sample .

The PostgreSQL database is integral for the inventory microservice as it handles the persistence of data. Run the following command to start the PostgreSQL database, which runs the postgres-sample image in a Docker container and maps 5432 port from the container to your host machine:

docker run --name postgres-container --rm -p 5432:5432 -d postgres-sample

Retrieve the PostgreSQL container IP address by running the following command:

docker inspect -f "{{.NetworkSettings.IPAddress }}" postgres-container

The command returns the PostgreSQL container IP address:

172.17.0.2

Now, navigate to the start directory to begin.

The Liberty Maven plug-in includes a devc goal that simplifies developing your application in a container by starting dev mode with container support. This goal builds a Docker image, mounts the required directories, binds the required ports, and then runs the application inside of a container. Dev mode also listens for any changes in the application source code or configuration and rebuilds the image and restarts the container as necessary.

Build and run the container by running the devc goal with the PostgreSQL container IP address. If your PostgreSQL container IP address is not 172.17.0.2, replace the command with the right IP address.

mvn liberty:devc -DcontainerRunOpts="-e DB_HOSTNAME=172.17.0.2" -DserverStartTimeout=240

Wait a moment for dev mode to start. Some error messages are expected as a result of building the docker image. Although these messages are included on the standard error stream, in this case they are not errors, just logs of the docker build progress. After you see the following message, your Liberty instance is ready in dev mode:

**************************************************************
*    Liberty is running in dev mode.
*    ...
*    Container network information:
*        Container name: [ liberty-dev ]
*        IP address [ 172.17.0.2 ] on container network [ bridge ]
*    ...

Dev mode holds your command-line session to listen for file changes. Open another command-line session to continue, or open the project in your editor.

Point your browser to the http://localhost:9080/openapi/ui URL to try out the inventory microservice manually. This interface provides a convenient visual way to interact with the APIs and test out their functionalities.

Building a REST test client

The REST test client is responsible for sending HTTP requests to an application and handling the responses. It enables accurate verification of the application’s behavior by ensuring that it responds correctly to various scenarios and conditions. Using a REST client for testing ensures reliable interaction with the inventory microservice across various deployment environments: local processes, Docker containers, or containers through Testcontainers.

Begin by creating a REST test client interface for the inventory microservice.

Create the SystemResourceClient class.
src/test/java/it/io/openliberty/guides/inventory/SystemResourceClient.java

SystemResourceClient.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2024 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package it.io.openliberty.guides.inventory;
13
14import java.util.List;
15
16import jakarta.enterprise.context.ApplicationScoped;
17import jakarta.ws.rs.Consumes;
18import jakarta.ws.rs.DELETE;
19import jakarta.ws.rs.GET;
20import jakarta.ws.rs.POST;
21import jakarta.ws.rs.PUT;
22import jakarta.ws.rs.Path;
23import jakarta.ws.rs.PathParam;
24import jakarta.ws.rs.Produces;
25import jakarta.ws.rs.QueryParam;
26import jakarta.ws.rs.core.MediaType;
27import jakarta.ws.rs.core.Response;
28
29
30@ApplicationScoped
31@Path("/systems")
32public interface SystemResourceClient {
33
34    // tag::listContents[]
35    @GET
36    @Path("/")
37    @Produces(MediaType.APPLICATION_JSON)
38    List<SystemData> listContents();
39    // end::listContents[]
40
41    // tag::getSystem[]
42    @GET
43    @Path("/{hostname}")
44    @Produces(MediaType.APPLICATION_JSON)
45    SystemData getSystem(
46        @PathParam("hostname") String hostname);
47    // end::getSystem[]
48
49    // tag::addSystem[]
50    @POST
51    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
52    @Produces(MediaType.APPLICATION_JSON)
53    Response addSystem(
54        @QueryParam("hostname") String hostname,
55        @QueryParam("osName") String osName,
56        @QueryParam("javaVersion") String javaVersion,
57        @QueryParam("heapSize") Long heapSize);
58    // end::addSystem[]
59
60    // tag::updateSystem[]
61    @PUT
62    @Path("/{hostname}")
63    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
64    @Produces(MediaType.APPLICATION_JSON)
65    Response updateSystem(
66        @PathParam("hostname") String hostname,
67        @QueryParam("osName") String osName,
68        @QueryParam("javaVersion") String javaVersion,
69        @QueryParam("heapSize") Long heapSize);
70    // end::updateSystem[]
71
72    // tag::removeSystem[]
73    @DELETE
74    @Path("/{hostname}")
75    @Produces(MediaType.APPLICATION_JSON)
76    Response removeSystem(
77        @PathParam("hostname") String hostname);
78    // end::removeSystem[]
79}

The SystemResourceClient interface declares the listContents(), getSystem(), addSystem(), updateSystem(), and removeSystem() methods for accessing the corresponding endpoints within the inventory microservice.

Next, create the SystemData data model for testing.

Create the SystemData class.
src/test/java/it/io/openliberty/guides/inventory/SystemData.java

SystemData.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2024 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package it.io.openliberty.guides.inventory;
13
14public class SystemData {
15
16    // tag::fields[]
17    private int id;
18    private String hostname;
19    private String osName;
20    private String javaVersion;
21    private Long heapSize;
22    // end::fields[]
23
24    public SystemData() {
25    }
26
27    // tag::getMethods[]
28    public int getId() {
29        return id;
30    }
31
32    public String getHostname() {
33        return hostname;
34    }
35
36    public String getOsName() {
37        return osName;
38    }
39
40    public String getJavaVersion() {
41        return javaVersion;
42    }
43
44    public Long getHeapSize() {
45        return heapSize;
46    }
47    // end::getMethods[]
48
49    // tag::setMethods[]
50    public void setId(int id) {
51        this.id = id;
52    }
53
54    public void setHostname(String hostname) {
55        this.hostname = hostname;
56    }
57
58    public void setOsName(String osName) {
59        this.osName = osName;
60    }
61
62    public void setJavaVersion(String javaVersion) {
63        this.javaVersion = javaVersion;
64    }
65
66    public void setHeapSize(Long heapSize) {
67        this.heapSize = heapSize;
68    }
69    // end::setMethods[]
70}

The SystemData class contains the ID, hostname, operating system name, Java version, and heap size properties. The various get and set methods within this class enable you to view and edit the properties of each system in the inventory.

Building a test container for Open Liberty

Next, create a custom class that extends Testcontainers' generic container to define specific configurations that suit your application’s requirements.

Define a custom LibertyContainer class, which provides a framework to start and access a containerized version of the Open Liberty application for testing.

Create the LibertyContainer class.
src/test/java/it/io/openliberty/guides/inventory/LibertyContainer.java

LibertyContainer.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2024 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package it.io.openliberty.guides.inventory;
13
14import org.testcontainers.containers.GenericContainer;
15import org.testcontainers.containers.wait.strategy.Wait;
16import org.testcontainers.images.builder.ImageFromDockerfile;
17
18// tag::GenericContainer[]
19public class LibertyContainer extends GenericContainer<LibertyContainer> {
20// end::GenericContainer[]
21
22    public LibertyContainer(ImageFromDockerfile image, int httpPort, int httpsPort) {
23
24        super(image);
25        // tag::addExposedPorts1[]
26        addExposedPorts(httpPort, httpsPort);
27        // end::addExposedPorts1[]
28
29        // wait for smarter planet message by default
30        // tag::waitingFor[]
31        waitingFor(Wait.forLogMessage("^.*CWWKF0011I.*$", 1));
32        // end::waitingFor[]
33
34    }
35
36    // tag::getBaseURL[]
37    public String getBaseURL() throws IllegalStateException {
38        return "http://" + getHost() + ":" + getFirstMappedPort();
39    }
40    // end::getBaseURL[]
41
42}

The LibertyContainer class extends the GenericContainer class from Testcontainers to create a custom container configuration specific to the Open Liberty application.

The addExposedPorts() method exposes specified ports from the container’s perspective, allowing test clients to communicate with services running inside the container. To avoid any port conflicts, Testcontainers assigns random host ports to these exposed container ports.

By default, the Wait.forLogMessage() method directs LibertyContainer to wait for the specific CWWKF0011I log message that indicates the Liberty instance has started successfully.

The getBaseURL() method contructs the base URL to access the container.

For more information about Testcontainers APIs and its functionality, refer to the Testcontainers JavaDocs.

Building test cases

Next, write tests that use the SystemResourceClient REST client and Testcontainers integration.

Create the SystemResourceIT class.
src/test/java/it/io/openliberty/guides/inventory/SystemResourceIT.java

SystemResourceIT.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2024 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package it.io.openliberty.guides.inventory;
 13
 14import static org.junit.jupiter.api.Assertions.assertEquals;
 15
 16import java.net.Socket;
 17import java.util.List;
 18import java.nio.file.Paths;
 19
 20import org.jboss.resteasy.client.jaxrs.ResteasyClient;
 21import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 22import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
 23import org.junit.jupiter.api.AfterAll;
 24import org.junit.jupiter.api.BeforeAll;
 25import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
 26import org.junit.jupiter.api.Order;
 27import org.junit.jupiter.api.Test;
 28import org.junit.jupiter.api.TestMethodOrder;
 29import org.slf4j.Logger;
 30import org.slf4j.LoggerFactory;
 31import org.testcontainers.containers.GenericContainer;
 32import org.testcontainers.containers.Network;
 33import org.testcontainers.containers.output.Slf4jLogConsumer;
 34import org.testcontainers.containers.wait.strategy.Wait;
 35import org.testcontainers.images.builder.ImageFromDockerfile;
 36
 37import jakarta.ws.rs.client.ClientBuilder;
 38import jakarta.ws.rs.core.UriBuilder;
 39
 40@TestMethodOrder(OrderAnnotation.class)
 41public class SystemResourceIT {
 42
 43    // tag::getLogger1[]
 44    private static Logger logger = LoggerFactory.getLogger(SystemResourceIT.class);
 45    // end::getLogger1[]
 46
 47    private static final String DB_HOST = "postgres";
 48    private static final int DB_PORT = 5432;
 49    // tag::postgresImage[]
 50    private static ImageFromDockerfile postgresImage
 51        = new ImageFromDockerfile("postgres-sample")
 52              .withDockerfile(Paths.get("../postgres/Dockerfile"));
 53    // end::postgresImage[]
 54
 55    private static int httpPort = Integer.parseInt(System.getProperty("http.port"));
 56    private static int httpsPort = Integer.parseInt(System.getProperty("https.port"));
 57    private static String contextRoot = System.getProperty("context.root") + "/api";
 58    // tag::invImage[]
 59    private static ImageFromDockerfile invImage
 60        = new ImageFromDockerfile("inventory:1.0-SNAPSHOT")
 61              .withDockerfile(Paths.get("./Dockerfile"));
 62    // end::invImage[]
 63
 64    private static SystemResourceClient client;
 65    // tag::network1[]
 66    private static Network network = Network.newNetwork();
 67    // end::network1[]
 68
 69    // tag::postgresContainer[]
 70    // tag::GenericContainer[]
 71    private static GenericContainer<?> postgresContainer
 72    // end::GenericContainer[]
 73        = new GenericContainer<>(postgresImage)
 74              // tag::network2[]
 75              .withNetwork(network)
 76              // end::network2[]
 77              .withExposedPorts(DB_PORT)
 78              .withNetworkAliases(DB_HOST)
 79              // tag::withLogConsumer1[]
 80              .withLogConsumer(new Slf4jLogConsumer(logger));
 81              // end::withLogConsumer1[]
 82    // end::postgresContainer[]
 83
 84    // tag::inventoryContainer[]
 85    // tag::LibertyContainer[]
 86    private static LibertyContainer inventoryContainer
 87    // end::LibertyContainer[]
 88        = new LibertyContainer(invImage, httpPort, httpsPort)
 89              .withEnv("DB_HOSTNAME", DB_HOST)
 90              // tag::network3[]
 91              .withNetwork(network)
 92              // end::network3[]
 93              // tag::waitingFor[]
 94              .waitingFor(Wait.forHttp("/health/ready").forPort(httpPort))
 95              // end::waitingFor[]
 96              // tag::withLogConsumer2[]
 97              .withLogConsumer(
 98                new Slf4jLogConsumer(
 99                    // tag::getLogger2[]
100                    LoggerFactory.getLogger(LibertyContainer.class)));
101                    // end::getLogger2[]
102              // end::withLogConsumer2[]
103    // end::inventoryContainer[]
104
105    // tag::isServiceRunning[]
106    private static boolean isServiceRunning(String host, int port) {
107        try {
108            Socket socket = new Socket(host, port);
109            socket.close();
110            return true;
111        } catch (Exception e) {
112            return false;
113        }
114    }
115    // end::isServiceRunning[]
116
117    // tag::createRestClient[]
118    private static SystemResourceClient createRestClient(String urlPath) {
119        ClientBuilder builder = ResteasyClientBuilder.newBuilder();
120        ResteasyClient client = (ResteasyClient) builder.build();
121        ResteasyWebTarget target = client.target(UriBuilder.fromPath(urlPath));
122        return target.proxy(SystemResourceClient.class);
123    }
124    // end::createRestClient[]
125
126    // tag::setup[]
127    @BeforeAll
128    public static void setup() throws Exception {
129        String urlPath;
130        if (isServiceRunning("localhost", httpPort)) {
131            logger.info("Testing by dev mode or local Liberty...");
132            if (isServiceRunning("localhost", DB_PORT)) {
133                logger.info("The application is ready to test.");
134                urlPath = "http://localhost:" + httpPort;
135            } else {
136                throw new Exception("Postgres database is not running");
137            }
138        } else {
139            logger.info("Testing by using Testcontainers...");
140            if (isServiceRunning("localhost", DB_PORT)) {
141                throw new Exception(
142                      "Postgres database is running locally. Stop it and retry.");
143            } else {
144                // tag::postgresContainerStart[]
145                postgresContainer.start();
146                // end::postgresContainerStart[]
147                // tag::inventoryContainerStart[]
148                inventoryContainer.start();
149                // end::inventoryContainerStart[]
150                urlPath = inventoryContainer.getBaseURL();
151            }
152        }
153        urlPath += contextRoot;
154        logger.info("TEST: " + urlPath);
155        client = createRestClient(urlPath);
156    }
157    // end::setup[]
158
159    // tag::tearDown[]
160    @AfterAll
161    public static void tearDown() {
162        inventoryContainer.stop();
163        postgresContainer.stop();
164        network.close();
165    }
166    // end::tearDown[]
167
168    private void showSystemData(SystemData system) {
169        logger.info("TEST: SystemData > "
170            + system.getId() + ", "
171            + system.getHostname() + ", "
172            + system.getOsName() + ", "
173            + system.getJavaVersion() + ", "
174            + system.getHeapSize());
175    }
176
177    // tag::testAddSystem[]
178    @Test
179    @Order(1)
180    public void testAddSystem() {
181        logger.info("TEST: Testing add a system");
182        // tag::addSystem[]
183        client.addSystem("localhost", "linux", "11", Long.valueOf(2048));
184        // end::addSystem[]
185        // tag::listContents[]
186        List<SystemData> systems = client.listContents();
187        // end::listContents[]
188        assertEquals(1, systems.size());
189        showSystemData(systems.get(0));
190        assertEquals("11", systems.get(0).getJavaVersion());
191        assertEquals(Long.valueOf(2048), systems.get(0).getHeapSize());
192    }
193    // end::testAddSystem[]
194
195    // tag::testUpdateSystem[]
196    @Test
197    @Order(2)
198    public void testUpdateSystem() {
199        logger.info("TEST: Testing update a system");
200        // tag::updateSystem[]
201        client.updateSystem("localhost", "linux", "8", Long.valueOf(1024));
202        // end::updateSystem[]
203        // tag::getSystem[]
204        SystemData system = client.getSystem("localhost");
205        // end::getSystem[]
206        showSystemData(system);
207        assertEquals("8", system.getJavaVersion());
208        assertEquals(Long.valueOf(1024), system.getHeapSize());
209    }
210    // end::testUpdateSystem[]
211
212    // tag::testRemoveSystem[]
213    @Test
214    @Order(3)
215    public void testRemoveSystem() {
216        logger.info("TEST: Testing remove a system");
217        // tag::removeSystem[]
218        client.removeSystem("localhost");
219        // end::removeSystem[]
220        List<SystemData> systems = client.listContents();
221        assertEquals(0, systems.size());
222    }
223    // end::testRemoveSystem[]
224}

LibertyContainer.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2024 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package it.io.openliberty.guides.inventory;
13
14import org.testcontainers.containers.GenericContainer;
15import org.testcontainers.containers.wait.strategy.Wait;
16import org.testcontainers.images.builder.ImageFromDockerfile;
17
18// tag::GenericContainer[]
19public class LibertyContainer extends GenericContainer<LibertyContainer> {
20// end::GenericContainer[]
21
22    public LibertyContainer(ImageFromDockerfile image, int httpPort, int httpsPort) {
23
24        super(image);
25        // tag::addExposedPorts1[]
26        addExposedPorts(httpPort, httpsPort);
27        // end::addExposedPorts1[]
28
29        // wait for smarter planet message by default
30        // tag::waitingFor[]
31        waitingFor(Wait.forLogMessage("^.*CWWKF0011I.*$", 1));
32        // end::waitingFor[]
33
34    }
35
36    // tag::getBaseURL[]
37    public String getBaseURL() throws IllegalStateException {
38        return "http://" + getHost() + ":" + getFirstMappedPort();
39    }
40    // end::getBaseURL[]
41
42}

postgres/Dockerfile

 1FROM postgres:16.2
 2
 3# set env variable for password to docker
 4ENV POSTGRES_PASSWORD adminpwd
 5# create database called database
 6ENV POSTGRES_USER admin
 7# Create the default database for PostgreSQL
 8ENV POSTGRES_DB admindb
 9
10# Use dumpfile to create table schema and populate it with data
11COPY schema.sql /docker-entrypoint-initdb.d/1-schema.sql
12
13EXPOSE 5432

Dockerfile

 1FROM maven:3.9.6 as staging
 2
 3WORKDIR /work
 4RUN mvn dependency:copy \
 5        -Dartifact=org.postgresql:postgresql:42.7.2 \
 6        -DoutputDirectory=/work
 7
 8FROM icr.io/appcafe/open-liberty:kernel-slim-java11-openj9-ubi
 9
10ARG VERSION=1.0
11ARG REVISION=SNAPSHOT
12
13LABEL \
14  org.opencontainers.image.authors="Your Name" \
15  org.opencontainers.image.vendor="Open Liberty" \
16  org.opencontainers.image.url="local" \
17  org.opencontainers.image.source="https://github.com/OpenLiberty/guide-testcontainers" \
18  org.opencontainers.image.version="$VERSION" \
19  org.opencontainers.image.revision="$REVISION" \
20  vendor="Open Liberty" \
21  name="inventory" \
22  version="$VERSION-$REVISION" \
23  summary="The inventory microservice from the Testcontainers guide" \
24  description="This image contains the inventory microservice running with the Open Liberty runtime."
25
26USER root
27
28COPY --chown=1001:0 \
29    src/main/liberty/config/ \
30    /config/
31
32RUN features.sh
33
34COPY --chown=1001:0 \
35    target/inventory.war \
36    /config/apps
37
38COPY --chown=1001:0  --from=staging \
39    /work/postgresql-*.jar \
40    /opt/ol/wlp/usr/shared/resources/
41
42USER 1001
43
44RUN configure.sh

Construct the postgresImage and invImage using the ImageFromDockerfile class, which allows Testcontainers to build Docker images from a Dockerfile during the test runtime. For these instances, the provided Dockerfiles at the specified paths ../postgres/Dockerfile and ./Dockerfile are used to generate the respective postgres-sample and inventory:1.0-SNAPSHOT images.

Use GenericContainer class to create the postgresContainer test container to start up the postgres-sample Docker image, and use the LibertyContainer custom class to create the inventoryContainer test container to start up the inventory:1.0-SNAPSHOT Docker image.

As containers are isolated by default, placing both the LibertyContainer and the postgresContainer on the same network allows them to communicate by using the hostname localhost and the internal port 5432, bypassing the need for an externally mapped port.

The waitingFor() method here overrides the waitingFor() method from LibertyContainer. Given that the inventory service depends on a database service, ensuring that readiness involves more than just the microservice itself. To address this, the inventoryContainer readiness is determined by checking the /health/ready health readiness check API, which reflects both the application and database service states. For different container readiness check customizations, see to the official Testcontainers documentation.

The LoggerFactory.getLogger() and withLogConsumer(new Slf4jLogConsumer(Logger)) methods integrate container logs with the test logs by piping the container output to the specified logger.

The createRestClient() method creates a REST client instance with the SystemResourceClient interface.

The setup() method prepares the test environment. It checks whether the test is running in dev mode or there is a local running Liberty instance, by using the isServiceRunning() helper. In the case of no running Liberty instance, the test starts the postgresContainer and inventoryContainer test containers. Otherwise, it ensures that the Postgres database is running locally.

The testAddSystem() verifies the addSystem and listContents endpoints.

The testUpdateSystem() verifies the updateSystem and getSystem endpoints.

The testRemoveSystem() verifies the removeSystem endpoint.

After the tests are executed, the tearDown() method stops the containers and closes the network.

Setting up logs

Having reliable logs is essential for efficient debugging, as they provide detailed insights into the test execution flow and help pinpoint issues during test failures. Testcontainers' built-in Slf4jLogConsumer enables integration of container output directly with the JUnit process, enhancing log analysis and simplifying test creation and debugging.

Create the log4j.properties file.
src/test/resources/log4j.properties

log4j.properties

 1log4j.rootLogger=INFO, stdout
 2
 3log4j.appender=org.apache.log4j.ConsoleAppender
 4log4j.appender.layout=org.apache.log4j.PatternLayout
 5
 6log4j.appender.stdout=org.apache.log4j.ConsoleAppender
 7log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
 8log4j.appender.stdout.layout.ConversionPattern=%r %p %c %x - %m%n
 9
10# tag::package[]
11log4j.logger.it.io.openliberty.guides.inventory=DEBUG
12# end::package[]

The log4j.properties file configures the root logger, appenders, and layouts for console output. It sets the logging level to DEBUG for the it.io.openliberty.guides.inventory package. This level provides detailed logging information for the specified package, which can be helpful for debugging and understanding test behavior.

Configuring the Maven project

Next, prepare your Maven project for test execution by adding the necessary dependencies for Testcontainers and logging, setting up Maven to copy the PostgreSQL JDBC driver during the build phase, and configuring the Liberty Maven Plugin to handle PostgreSQL dependency.

Replace the pom.xml file.
pom.xml

pom.xml

  1<?xml version="1.0" encoding="UTF-8" ?>
  2<project xmlns="http://maven.apache.org/POM/4.0.0"
  3         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5
  6    <modelVersion>4.0.0</modelVersion>
  7
  8    <groupId>io.openliberty.guides</groupId>
  9    <artifactId>guide-testcontainers</artifactId>
 10    <packaging>war</packaging>
 11    <version>1.0-SNAPSHOT</version>
 12
 13    <properties>
 14        <maven.compiler.source>11</maven.compiler.source>
 15        <maven.compiler.target>11</maven.compiler.target>
 16        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 17        <liberty.var.http.port>9080</liberty.var.http.port>
 18        <liberty.var.https.port>9443</liberty.var.https.port>
 19        <liberty.var.context.root>/inventory</liberty.var.context.root>
 20    </properties>
 21
 22    <dependencies>
 23        <dependency>
 24            <groupId>jakarta.platform</groupId>
 25            <artifactId>jakarta.jakartaee-api</artifactId>
 26            <version>10.0.0</version>
 27            <scope>provided</scope>
 28        </dependency>
 29        <dependency>
 30            <groupId>org.eclipse.microprofile</groupId>
 31            <artifactId>microprofile</artifactId>
 32            <version>6.1</version>
 33            <type>pom</type>
 34            <scope>provided</scope>
 35        </dependency>
 36        <dependency>
 37            <groupId>org.postgresql</groupId>
 38            <artifactId>postgresql</artifactId>
 39            <version>42.7.4</version>
 40            <scope>provided</scope>
 41        </dependency>
 42        
 43        <!-- tag::testDependenies[] -->
 44        <!-- Test dependencies -->
 45        <dependency>
 46            <groupId>org.junit.jupiter</groupId>
 47            <artifactId>junit-jupiter</artifactId>
 48            <version>5.11.3</version>
 49            <scope>test</scope>
 50        </dependency>
 51        <dependency>
 52            <groupId>org.jboss.resteasy</groupId>
 53            <artifactId>resteasy-client</artifactId>
 54            <version>6.2.10.Final</version>
 55            <scope>test</scope>
 56        </dependency>
 57        <dependency>
 58            <groupId>org.jboss.resteasy</groupId>
 59            <artifactId>resteasy-json-binding-provider</artifactId>
 60            <version>6.2.10.Final</version>
 61            <scope>test</scope>
 62        </dependency>
 63        <dependency>
 64            <groupId>org.glassfish</groupId>
 65            <artifactId>jakarta.json</artifactId>
 66            <version>2.0.1</version>
 67            <scope>test</scope>
 68        </dependency>
 69        <dependency>
 70            <groupId>org.eclipse</groupId>
 71            <artifactId>yasson</artifactId>
 72            <version>3.0.4</version>
 73            <scope>test</scope>
 74        </dependency>
 75        <!-- tag::testcontainers[] -->
 76        <dependency>
 77            <groupId>org.testcontainers</groupId>
 78            <artifactId>testcontainers</artifactId>
 79            <version>1.20.3</version>
 80            <scope>test</scope>
 81        </dependency>
 82        <!-- end::testcontainers[] -->
 83        <!-- tag::slf4j[] -->
 84        <dependency>
 85            <groupId>org.slf4j</groupId>
 86            <artifactId>slf4j-reload4j</artifactId>
 87            <version>2.0.16</version>
 88            <scope>test</scope>
 89        </dependency>
 90        <!-- end::slf4j[] -->
 91        <!-- tag::slf4j-api[] -->
 92        <dependency>
 93            <groupId>org.slf4j</groupId>
 94            <artifactId>slf4j-api</artifactId>
 95            <version>2.0.16</version>
 96        </dependency>
 97        <!-- end::slf4j-api[] -->
 98        <!-- end::testDependenies[] -->
 99    </dependencies>
100
101    <build>
102        <finalName>inventory</finalName>
103        <plugins>
104            <plugin>
105                <groupId>org.apache.maven.plugins</groupId>
106                <artifactId>maven-war-plugin</artifactId>
107                <version>3.4.0</version>
108            </plugin>
109            <plugin>
110                <groupId>io.openliberty.tools</groupId>
111                <artifactId>liberty-maven-plugin</artifactId>
112                <configuration>
113                    <copyDependencies>
114                        <dependencyGroup>
115                            <location>${project.build.directory}/liberty/wlp/usr/shared/resources</location>
116                            <dependency>
117                                <groupId>org.postgresql</groupId>
118                                <artifactId>postgresql</artifactId>
119                            </dependency>
120                        </dependencyGroup>
121                    </copyDependencies>
122                </configuration>
123                <version>3.11.1</version>
124            </plugin>
125            <!-- tag::failsafe[] -->
126            <plugin>
127                <groupId>org.apache.maven.plugins</groupId>
128                <artifactId>maven-failsafe-plugin</artifactId>
129                <version>3.5.1</version>
130                <configuration>
131                    <systemPropertyVariables>
132                        <http.port>${liberty.var.http.port}</http.port>
133                        <https.port>${liberty.var.https.port}</https.port>
134                        <context.root>${liberty.var.context.root}</context.root>
135                    </systemPropertyVariables>
136                </configuration>
137                <executions>
138                    <execution>
139                        <goals>
140                            <goal>integration-test</goal>
141                            <goal>verify</goal>
142                        </goals>
143                    </execution>
144                </executions>
145            </plugin>
146            <!-- end::failsafe[] -->
147        </plugins>
148    </build>
149</project>

Add the required dependency for Testcontainers and Log4J libraries with test scope. The testcontainers dependency offers a general-purpose API for managing container-based test environments. The slf4j-reload4j and slf4j-api dependencies enable the Simple Logging Facade for Java (SLF4J) API for trace logging during test execution and facilitates debugging and test performance tracking.

Also, add and configure the maven-failsafe-plugin plugin, so that the integration test can be run by the mvn verify command.

When you started Open Liberty in dev mode, all the changes were automatically picked up. You can run the tests by pressing the enter/return key from the command-line session where you started dev mode. You see the following output:

 -------------------------------------------------------
  T E S T S
 -------------------------------------------------------
 Running it.io.openliberty.guides.inventory.SystemResourceIT
 it.io.openliberty.guides.inventory.SystemResourceIT  - Testing by dev mode or local Liberty...
 ...
 Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.873 s - in it.io.openliberty.guides.inventory.SystemResourceIT

 Results:

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

Running tests in a CI/CD pipeline

Running tests in dev mode is useful for local development, but there may be times when you want to test your application in other scenarios, such as in a CI/CD pipeline. For these cases, you can use Testcontainers to run tests against a running Open Liberty instance in a controlled, self-contained environment, ensuring that your tests run consistently regardless of the deployment context.

To test outside of dev mode, exit dev mode by pressing CTRL+C in the command-line session where you ran the Liberty.

Also, run the following commands to stop the PostgreSQL container that was started in the previous section:

docker stop postgres-container

Now, use the following Maven goal to run the tests from a cold start outside of dev mode:

mvn clean verify
export TESTCONTAINERS_RYUK_DISABLED=true
mvn clean verify

You see the following output:

 -------------------------------------------------------
  T E S T S
 -------------------------------------------------------
 Running it.io.openliberty.guides.inventory.SystemResourceIT
 it.io.openliberty.guides.inventory.SystemResourceIT  - Testing by using Testcontainers...
 ...
 tc.postgres-sample:latest  - Creating container for image: postgres-sample:latest
 tc.postgres-sample:latest  - Container postgres-sample:latest is starting: 7cf2e2c6a505f41877014d08b7688399b3abb9725550e882f1d33db8fa4cff5a
 tc.postgres-sample:latest  - Container postgres-sample:latest started in PT2.925405S
 ...
 tc.inventory:1.0-SNAPSHOT  - Creating container for image: inventory:1.0-SNAPSHOT
 tc.inventory:1.0-SNAPSHOT  - Container inventory:1.0-SNAPSHOT is starting: 432ac739f377abe957793f358bbb85cc916439283ed2336014cacb585f9992b8
 tc.inventory:1.0-SNAPSHOT  - Container inventory:1.0-SNAPSHOT started in PT25.784899S
...

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.208 s - in it.io.openliberty.guides.inventory.SystemResourceIT

Results:

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

Notice that the test initiates a new Docker container each for the PostgreSQL database and the inventory microservice, resulting in a longer test runtime. Despite this, cold start testing benefits from a clean instance per run and ensures consistent results. These tests also automatically hook into existing build pipelines that are set up to run the integration-test phase.

Great work! You’re done!

You just tested your microservices with multiple Docker containers using Testcontainers.

Guide Attribution

Building true-to-production integration tests with Testcontainers by Open Liberty is licensed under CC BY-ND 4.0

Copy file contents
Copied to clipboard

Prerequisites:

Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star