Building true-to-production integration tests with Testcontainers
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.
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:
WINDOWS
MAC
LINUX
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 theSystemResourceClient
class.src/test/java/it/io/openliberty/guides/inventory/SystemResourceClient.java
SystemResourceClient.java
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 theSystemData
class.src/test/java/it/io/openliberty/guides/inventory/SystemData.java
SystemData.java
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 theLibertyContainer
class.src/test/java/it/io/openliberty/guides/inventory/LibertyContainer.java
LibertyContainer.java
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 theSystemResourceIT
class.src/test/java/it/io/openliberty/guides/inventory/SystemResourceIT.java
SystemResourceIT.java
LibertyContainer.java
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 thelog4j.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 thepom.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:
WINDOWS
MAC
LINUX
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
Prerequisites:
Nice work! Where to next?
What did you think of this guide?
Thank you for your feedback!
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