Skip to content

A guide on how to test your microservices with multiple containers by using Testcontainers and JUnit.

License

Notifications You must be signed in to change notification settings

scottkurz/guide-testcontainers

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

83 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Building true-to-production integration tests with Testcontainers

Note
This repository contains the guide documentation source. To view the guide in published form, see the Open Liberty website.

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.

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 mvn package command to build and package the application, which places the .war file in the target directory and the .jar PostgreSQL JDBC driver file in the target/liberty/wlp/usr/shared/resources directory:

cd finish
mvn package

Now, run the mvn verify command, which compiles the Java files, starts the containers, runs the tests, and then stops the containers.

WINDOWS

MAC

LINUX

mvn verify
export TESTCONTAINERS_RYUK_DISABLED=true
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.

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. 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.3 ] 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

link:finish/src/test/java/it/io/openliberty/guides/inventory/SystemResourceClient.java[role=include]

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

link:finish/src/test/java/it/io/openliberty/guides/inventory/SystemData.java[role=include]

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

link:finish/src/test/java/it/io/openliberty/guides/inventory/LibertyContainer.java[role=include]

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

link:finish/src/test/java/it/io/openliberty/guides/inventory/SystemResourceIT.java[role=include]

LibertyContainer.java

link:finish/src/test/java/it/io/openliberty/guides/inventory/LibertyContainer.java[role=include]

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

link:finish/src/test/resources/log4j.properties[role=include]

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

link:finish/pom.xml[role=include]

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.

The Maven pom.xml file contains a configuration for the maven-dependency-plugin to copy the PostgreSQL JDBC driver into the Liberty configuration’s shared resources directory. This setup occurs during the prepare-package phase. As a result, running the mvn package command ensures the PostgreSQL driver is prepared and accessible for your application when it runs on the Liberty.

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 verify
export TESTCONTAINERS_RYUK_DISABLED=true
mvn 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.

About

A guide on how to test your microservices with multiple containers by using Testcontainers and JUnit.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 88.5%
  • Dockerfile 6.0%
  • Shell 5.5%