Testing reactive Java microservices
Prerequisites:
Learn how to test reactive Java microservices in true-to-production environments using Testcontainers.
What you’ll learn
You will learn how to write integration tests for reactive Java microservices and to run the tests in true-to-production environments by using containers with Testcontainers and JUnit. Testcontainers tests your containerized application from outside the container so that you are testing the exact same image that runs in production. The reactive application in this guide sends and receives messages between services by using an external message broker, Apache Kafka. Using an external message broker enables asynchronous communications between services so that requests are non-blocking and decoupled from responses. You can learn more about reactive Java services that use an external message broker to manage communications in the Creating reactive Java microservices guide.
True-to-production integration testing with Testcontainers
Tests sometimes pass during the development and testing stages of an application’s lifecycle but then fail in production because of differences between your development and production environments. While you can create mock objects and custom setups to minimize differences between environments, it is difficult to mimic a production system for an application that uses an external messaging system. Testcontainers addresses this problem by enabling the testing of applications in the same Docker containers that you’ll use in production. As a result, your environment remains the same throughout the application’s lifecycle – from development, through testing, and into production. You can learn more about Testcontainers in the Building true-to-production integration tests with Testcontainers guide.
Additional prerequisites
You need to have Docker installed. For installation instructions, refer to the official Docker documentation. You will build and run the microservices in Docker containers. An installation of Apache Kafka is provided in another Docker container.
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-reactive-service-testing.git
cd guide-reactive-service-testing
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 tests, go to the finish
directory and run the following Maven goal to install the models
artifact to the local Maven repository:
cd finish
mvn -pl models install
Next, navigate to the finish/system
directory and run the following Maven goal to build the system
microservice and run the integration tests on an Open Liberty server in a container:
WINDOWS
MAC
LINUX
cd system
mvn verify
export TESTCONTAINERS_RYUK_DISABLED=true
cd system
mvn verify
You will see the following output:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 52.46 s - in it.io.openliberty.guides.system.SystemServiceIT
Results:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
--- failsafe:3.2.5:verify (verify) @ system ---
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 57.710 s
Finished at: 2024-02-01T08:48:15-08:00
------------------------------------------------------------------------
This command might take some time to run the first time because the dependencies and the Docker image for Open Liberty must download. If you run the same command again, it will be faster.
You can also try out the inventory
integration tests by repeating the same commands in the finish/inventory
directory.
Testing with the Kafka consumer client
system/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 <modelVersion>4.0.0</modelVersion>
6
7 <groupId>io.openliberty.guides</groupId>
8 <artifactId>system</artifactId>
9 <version>1.0-SNAPSHOT</version>
10 <packaging>war</packaging>
11
12 <properties>
13 <maven.compiler.source>11</maven.compiler.source>
14 <maven.compiler.target>11</maven.compiler.target>
15 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
17 <!-- Liberty configuration -->
18 <liberty.var.http.port>9083</liberty.var.http.port>
19 <liberty.var.https.port>9446</liberty.var.https.port>
20 </properties>
21
22 <dependencies>
23 <!-- Provided dependencies -->
24 <dependency>
25 <groupId>jakarta.platform</groupId>
26 <artifactId>jakarta.jakartaee-api</artifactId>
27 <version>10.0.0</version>
28 <scope>provided</scope>
29 </dependency>
30 <dependency>
31 <groupId>org.eclipse.microprofile</groupId>
32 <artifactId>microprofile</artifactId>
33 <version>6.1</version>
34 <type>pom</type>
35 <scope>provided</scope>
36 </dependency>
37 <dependency>
38 <groupId>org.eclipse.microprofile.reactive.messaging</groupId>
39 <artifactId>microprofile-reactive-messaging-api</artifactId>
40 <version>3.0</version>
41 <scope>provided</scope>
42 </dependency>
43
44 <!-- Required dependencies -->
45 <dependency>
46 <groupId>io.openliberty.guides</groupId>
47 <artifactId>models</artifactId>
48 <version>1.0-SNAPSHOT</version>
49 </dependency>
50 <dependency>
51 <groupId>org.apache.kafka</groupId>
52 <artifactId>kafka-clients</artifactId>
53 <version>3.8.1</version>
54 </dependency>
55 <dependency>
56 <groupId>io.reactivex.rxjava3</groupId>
57 <artifactId>rxjava</artifactId>
58 <version>3.1.9</version>
59 </dependency>
60
61 <!-- For tests -->
62 <dependency>
63 <groupId>org.slf4j</groupId>
64 <artifactId>slf4j-api</artifactId>
65 <version>2.0.16</version>
66 </dependency>
67 <dependency>
68 <groupId>org.slf4j</groupId>
69 <artifactId>slf4j-simple</artifactId>
70 <version>2.0.16</version>
71 </dependency>
72 <!-- tag::dependencies[] -->
73 <dependency>
74 <groupId>org.testcontainers</groupId>
75 <artifactId>kafka</artifactId>
76 <version>1.20.3</version>
77 <scope>test</scope>
78 </dependency>
79 <dependency>
80 <groupId>org.junit.jupiter</groupId>
81 <artifactId>junit-jupiter</artifactId>
82 <version>5.11.3</version>
83 <scope>test</scope>
84 </dependency>
85 <dependency>
86 <groupId>org.testcontainers</groupId>
87 <artifactId>junit-jupiter</artifactId>
88 <version>1.20.3</version>
89 <scope>test</scope>
90 </dependency>
91 <!-- end::dependencies[] -->
92 </dependencies>
93
94 <build>
95 <finalName>${project.artifactId}</finalName>
96 <plugins>
97 <plugin>
98 <groupId>org.apache.maven.plugins</groupId>
99 <artifactId>maven-war-plugin</artifactId>
100 <version>3.4.0</version>
101 <configuration>
102 <packagingExcludes>pom.xml</packagingExcludes>
103 </configuration>
104 </plugin>
105
106 <!-- Liberty plugin -->
107 <plugin>
108 <groupId>io.openliberty.tools</groupId>
109 <artifactId>liberty-maven-plugin</artifactId>
110 <version>3.11.1</version>
111 <configuration>
112 <!-- tag::devc_config[] -->
113 <!-- devc config -->
114 <containerRunOpts>
115 -p 9083:9083
116 <!-- tag::reactive-app[] -->
117 --network=reactive-app
118 <!-- end::reactive-app[] -->
119 </containerRunOpts>
120 <!-- end::devc_config[] -->
121 </configuration>
122 </plugin>
123
124 <!-- Plugin to run unit tests -->
125 <plugin>
126 <groupId>org.apache.maven.plugins</groupId>
127 <artifactId>maven-surefire-plugin</artifactId>
128 <version>3.5.1</version>
129 </plugin>
130
131 <!-- Plugin to run integration tests -->
132 <plugin>
133 <groupId>org.apache.maven.plugins</groupId>
134 <artifactId>maven-failsafe-plugin</artifactId>
135 <version>3.5.1</version>
136 <executions>
137 <execution>
138 <id>integration-test</id>
139 <goals>
140 <goal>integration-test</goal>
141 </goals>
142 <configuration>
143 <trimStackTrace>false</trimStackTrace>
144 </configuration>
145 </execution>
146 <execution>
147 <id>verify</id>
148 <goals>
149 <goal>verify</goal>
150 </goals>
151 </execution>
152 </executions>
153 </plugin>
154 </plugins>
155 </build>
156</project>
inventory/pom.xml
1<?xml version='1.0' encoding='utf-8'?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3 <modelVersion>4.0.0</modelVersion>
4
5 <groupId>io.openliberty.guides</groupId>
6 <artifactId>inventory</artifactId>
7 <version>1.0-SNAPSHOT</version>
8 <packaging>war</packaging>
9
10 <properties>
11 <maven.compiler.source>11</maven.compiler.source>
12 <maven.compiler.target>11</maven.compiler.target>
13 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
14 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
15 <!-- Liberty configuration -->
16 <liberty.var.http.port>9085</liberty.var.http.port>
17 <liberty.var.https.port>9448</liberty.var.https.port>
18 </properties>
19
20 <dependencies>
21 <!-- Provided dependencies -->
22 <dependency>
23 <groupId>jakarta.platform</groupId>
24 <artifactId>jakarta.jakartaee-api</artifactId>
25 <version>10.0.0</version>
26 <scope>provided</scope>
27 </dependency>
28 <dependency>
29 <groupId>jakarta.enterprise.concurrent</groupId>
30 <artifactId>jakarta.enterprise.concurrent-api</artifactId>
31 <version>3.0.3</version>
32 <scope>provided</scope>
33 </dependency>
34 <dependency>
35 <groupId>org.eclipse.microprofile</groupId>
36 <artifactId>microprofile</artifactId>
37 <version>6.1</version>
38 <type>pom</type>
39 <scope>provided</scope>
40 </dependency>
41 <dependency>
42 <groupId>org.eclipse.microprofile.reactive.messaging</groupId>
43 <artifactId>microprofile-reactive-messaging-api</artifactId>
44 <version>3.0</version>
45 <scope>provided</scope>
46 </dependency>
47
48 <!-- Required dependencies -->
49 <dependency>
50 <groupId>io.openliberty.guides</groupId>
51 <artifactId>models</artifactId>
52 <version>1.0-SNAPSHOT</version>
53 </dependency>
54 <dependency>
55 <groupId>org.apache.kafka</groupId>
56 <artifactId>kafka-clients</artifactId>
57 <version>3.8.1</version>
58 </dependency>
59
60 <!-- For tests -->
61 <dependency>
62 <groupId>org.slf4j</groupId>
63 <artifactId>slf4j-api</artifactId>
64 <version>2.0.16</version>
65 </dependency>
66 <dependency>
67 <groupId>org.slf4j</groupId>
68 <artifactId>slf4j-simple</artifactId>
69 <version>2.0.16</version>
70 </dependency>
71 <!-- tag::dependencies[] -->
72 <dependency>
73 <groupId>org.testcontainers</groupId>
74 <artifactId>kafka</artifactId>
75 <version>1.20.3</version>
76 <scope>test</scope>
77 </dependency>
78 <dependency>
79 <groupId>org.junit.jupiter</groupId>
80 <artifactId>junit-jupiter</artifactId>
81 <version>5.11.3</version>
82 <scope>test</scope>
83 </dependency>
84 <dependency>
85 <groupId>org.testcontainers</groupId>
86 <artifactId>junit-jupiter</artifactId>
87 <version>1.20.3</version>
88 <scope>test</scope>
89 </dependency>
90 <!-- end::dependencies[] -->
91 <dependency>
92 <groupId>org.jboss.resteasy</groupId>
93 <artifactId>resteasy-client</artifactId>
94 <version>6.2.10.Final</version>
95 <scope>test</scope>
96 </dependency>
97 <dependency>
98 <groupId>org.jboss.resteasy</groupId>
99 <artifactId>resteasy-json-binding-provider</artifactId>
100 <version>6.2.10.Final</version>
101 <scope>test</scope>
102 </dependency>
103 </dependencies>
104
105 <build>
106 <finalName>${project.artifactId}</finalName>
107 <plugins>
108 <plugin>
109 <groupId>org.apache.maven.plugins</groupId>
110 <artifactId>maven-war-plugin</artifactId>
111 <version>3.4.0</version>
112 <configuration>
113 <packagingExcludes>pom.xml</packagingExcludes>
114 </configuration>
115 </plugin>
116
117 <!-- Liberty plugin -->
118 <plugin>
119 <groupId>io.openliberty.tools</groupId>
120 <artifactId>liberty-maven-plugin</artifactId>
121 <version>3.11.1</version>
122 <configuration>
123 <!-- devc config -->
124 <containerRunOpts>
125 -p 9085:9085
126 --network=reactive-app
127 </containerRunOpts>
128 </configuration>
129 </plugin>
130
131 <!-- Plugin to run unit tests -->
132 <plugin>
133 <groupId>org.apache.maven.plugins</groupId>
134 <artifactId>maven-surefire-plugin</artifactId>
135 <version>3.5.1</version>
136 </plugin>
137
138 <!-- Plugin to run integration tests -->
139 <plugin>
140 <groupId>org.apache.maven.plugins</groupId>
141 <artifactId>maven-failsafe-plugin</artifactId>
142 <version>3.5.1</version>
143 <executions>
144 <execution>
145 <goals>
146 <goal>integration-test</goal>
147 <goal>verify</goal>
148 </goals>
149 </execution>
150 </executions>
151 </plugin>
152 </plugins>
153 </build>
154</project>
system/microprofile-config.properties
startKafka.sh
1#!/bin/bash
2
3# tag::dockerNetworkSetup[]
4NETWORK=reactive-app
5docker network create $NETWORK
6# end::dockerNetworkSetup[]
7
8docker run -d \
9 -e ALLOW_PLAINTEXT_LISTENER=yes \
10 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,LOCAL://localhost:9094 \
11 -e KAFKA_CFG_NODE_ID=0 \
12 -e KAFKA_CFG_PROCESS_ROLES=controller,broker \
13 -e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,LOCAL://:9094 \
14 -e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,LOCAL:PLAINTEXT \
15 -e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 \
16 -e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
17 -p 9094:9094 \
18 --network=$NETWORK \
19 --name=kafka \
20 --rm \
21 bitnami/kafka:latest &
22
23wait
SystemServiceIT.java
Navigate to the start
directory to begin.
The example reactive application consists of the system
and inventory
microservices. The system
microservice produces messages to the Kafka message broker, and the inventory
microservice consumes messages from the Kafka message broker. You will write integration tests to see how you can use the Kafka consumer and producer client APIs to test each service. Kafka test containers, Testcontainers, and JUnit are already included as required test dependencies in your Maven pom.xml
files for the system
and inventory
microservices.
The start
directory contains three directories: the system
microservice directory, the inventory
microservice directory, and the models
directory. The models
directory contains the model class that defines the structure of the system load data that is used in the application. Run the following Maven goal to install the packaged models
artifact to the local Maven repository so it can be used later by the system
and inventory
microservices:
mvn -pl models install
Launching the system microservice in dev mode with container support
Start the microservices in dev mode by running the following command to launch a Kafka instance that replicates the production environment. The startKafka
script launches a local Kafka container. It also establishes a reactive-app
network that allows the system
and inventory
microservices to connect to the Kafka message broker.
WINDOWS
MAC
LINUX
.\scripts\startKafka.bat
./scripts/startKafka.sh
Navigate to the start/system
directory.
To launch the system
microservice in dev mode with container support, configure the container by specifying the options within the <containerRunOpts>
element to connect to the reactive-app
network and expose the container port.
Run the following goal to start the system
microservice in dev mode with container support:
WINDOWS
MAC
LINUX
mvn liberty:devc
export TESTCONTAINERS_RYUK_DISABLED=true
mvn liberty:devc
For more information about disabling Ryuk, see the Testcontainers custom configuration document.
After you see the following message, your Liberty instance is ready in dev mode:
************************************************************** * Liberty is running in dev mode. * ... * Liberty container port information: * Internal container HTTP port [ 9083 ] is mapped to container host port [ 9083 ] < * ...
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.
The system
microservice actively seeks a Kafka topic for message push operations. After the Kafka service starts, the system
microservice connects to the Kafka message broker by using the mp.messaging.connector.liberty-kafka.bootstrap.servers
property. When you run your application in dev mode with container support, the running system
container exposes its service on the 9083
port for testing purposes.
Testing the system microservice
Now you can start writing the test by using Testcontainers.
Create theSystemServiceIT
class.system/src/test/java/it/io/openliberty/guides/system/SystemServiceIT.java
SystemServiceIT.java
SystemLoad.java
SystemService.java
Dockerfile
Construct the systemImage
by using the ImageFromDockerfile
class, which allows Testcontainers to build the Docker image from a Dockerfile during the test run time. For instance, the provided Dockerfile at the specified ./Dockerfile
paths is used to generate the system:1.0-SNAPSHOT
image.
Use the kafkaContainer
class to instantiate the kafkaContainer
test container, initiating the confluentinc/cp-kafka:latest
Docker image. Similarly, use the GenericContainer
class to create the systemContainer
test container, starting the system:1.0-SNAPSHOT
Docker image.
The withListener()
is configured to kafka:19092
, as the containerized system
microservice functions as an additional producer. Therefore, the Kafka container needs to set up a listener to accommodate this requirement. For more information about using an additional consumer or producer with a Kafka container, see the Testcontainers Kafka documentation
Because containers are isolated by default, facilitating communication between the kafkaContainer
and the systemContainer
requires placing them on the same network
. The dependsOn()
method is used to indicate that the system
microservice container starts only after ensuring the readiness of the Kafka container.
Before you start the systemContainer
, you must override the mp.messaging.connector.liberty-kafka.bootstrap.servers
property with kafka:19092
by using the withEnv()
method. This step creates a listener in the Kafka container that is configured to handle an additional producer.
The test uses the KafkaConsumer
client API, configuring the consumer to use the BOOTSTRAP_SERVERS_CONFIG
property with the Kafka broker address if a local system
microservice container is present. In the absence of a local service container, it uses the getBootstrapServers()
method to obtain the broker address from the Kafka test container. Then, the consumer is set up to consume messages from the system.load
topic within the Kafka
container.
To consume messages from a stream, the messages need to be deserialized from bytes. Kafka has its own default deserializer, but a custom deserializer is provided for you. The deserializer is configured by the VALUE_DESERIALIZER_CLASS_CONFIG
property and is implemented in the SystemLoad
class. To learn more about Kafka APIs and their usage, see the official Kafka Documentation.
The running system
microservice container produces messages to the systemLoad
Kafka topic, as denoted by the @Outgoing
annotation. The testCpuStatus()
test method uses the consumer.poll()
method from the KafkaConsumer
client API to retrieve a record from Kafka every 3 seconds within a specified timeout limit. This record is produced by the system service. Then, the method uses Assertions
to verify that the polled record aligns with the expected record.
Running the tests
Because you started Open Liberty in dev mode, you can run the tests by pressing the enter/return
key from the command-line session where you started dev mode.
You will see the following output:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 25.674 s - in it.io.openliberty.guides.system.SystemServiceIT
Results:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Integration tests finished.
After you are finished running tests, stop the Open Liberty server by pressing CTRL+C
in the command-line session where you ran the server.
If you aren’t running in dev mode, you can run the tests by running the following command:
mvn clean verify
You will see the following output:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 50.63 s - in it.io.openliberty.guides.system.SystemServiceIT
Results:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
--- failsafe:3.2.5:verify (verify) @ system ---
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 55.636 s
Finished at: 2024-01-31T11:33:40-08:00
------------------------------------------------------------------------
Testing with the Kafka producer client
The inventory
microservice is tested in the same way as the system
microservice. The only difference is that the inventory
microservice consumes messages, which means that tests are written to use the Kafka producer client.
Launching the inventory microservice in dev mode with container
Navigate to the start/inventory
directory.
Run the following goal to start the inventory
microservice in dev mode with container support:
WINDOWS
MAC
LINUX
mvn liberty:devc
mvn liberty:devc
Building a test REST client
Create a REST client interface to access the inventory
microservice.
Create theInventoryResourceClient
class.inventory/src/test/java/it/io/openliberty/guides/inventory/InventoryResourceClient.java
InventoryResourceClient.java
The InventoryResourceClient
interface declares the getSystems()
and resetSystems()
methods for accessing the corresponding endpoints within the inventory
microservice.
Testing the inventory microservice
Now you can start writing the test by using Testcontainers.
Create theInventoryServiceIT
class.inventory/src/test/java/it/io/openliberty/guides/inventory/InventoryServiceIT.java
InventoryServiceIT.java
SystemLoad.java
InventoryResource.java
The InventoryServiceIT
class uses the KafkaProducer
client API to generate messages in the test environment, which are then consumed by the inventory
microservice container.
Similar to system
microservice testing, the configuration of the producer BOOTSTRAP_SERVERS_CONFIG
property depends on whether a local inventory
microservice container is detected. In addition, the producer is configured with a custom serializer provided in the SystemLoad
class.
The testCpuUsage
test method uses the producer.send()
method, using the KafkaProducer
client API, to generate the Systemload
message. Then, it uses Assertions
to verify that the response from the inventory
microservice aligns with the expected outcome.
Running the tests
Because you started Open Liberty in dev mode, you can run the tests by pressing the enter/return
key from the command-line session where you started dev mode.
You will see the following output:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 32.564 s - in it.io.openliberty.guides.inventory.InventoryServiceIT
Results:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Integration tests finished.
After you are finished running tests, stop the Open Liberty server by pressing CTRL+C
in the command-line session where you ran the server.
If you aren’t running in dev mode, you can run the tests by running the following command:
mvn clean verify
You will see the following output:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 53.22 s - in it.io.openliberty.guides.inventory.InventoryServiceIT
Results:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
--- failsafe:3.2.5:verify (verify) @ inventory ---
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 58.789 s
Finished at: 2024-01-31T11:40:43-08:00
------------------------------------------------------------------------
When you’re finished trying out the microservice, you can stop the local Kafka container by running the following command from the start
directory:
WINDOWS
MAC
LINUX
.\scripts\stopKafka.bat
./scripts/stopKafka.sh
Great work! You’re done!
You just tested two reactive Java microservices using Testcontainers.
Related Links
Learn more about Testcontainers.
Guide Attribution
Testing reactive Java microservices 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