Configure a complete testing framework for Kafka Streams applications using TopologyTestDriver to validate stream processing logic with automated tests and mock data pipelines.
Prerequisites
- Java 11 or later
- Maven or Gradle build tool
- Basic understanding of Apache Kafka concepts
What this solves
Testing Kafka Streams applications in production environments is complex and expensive. The TopologyTestDriver provides a lightweight testing framework that simulates Kafka brokers and topics without requiring a full Kafka cluster. This enables fast, automated unit tests for stream processing logic, topology validation, and data transformation pipelines.
Step-by-step installation
Install Java Development Kit
Kafka Streams requires Java 11 or later for development and testing.
sudo apt update
sudo apt install -y openjdk-17-jdk maven gradle
Download and install Apache Kafka
Install Kafka with Scala 2.13 for Streams API compatibility.
cd /opt
sudo wget https://archive.apache.org/dist/kafka/2.8.2/kafka_2.13-2.8.2.tgz
sudo tar -xzf kafka_2.13-2.8.2.tgz
sudo mv kafka_2.13-2.8.2 kafka
sudo chown -R $(whoami):$(whoami) /opt/kafka
Set up environment variables
Configure Java and Kafka paths for development tools.
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export KAFKA_HOME=/opt/kafka
export PATH=$PATH:$KAFKA_HOME/bin:$JAVA_HOME/bin
source ~/.bashrc
java -version
echo $KAFKA_HOME
Create Maven project structure
Set up a Maven project with proper directory structure for Kafka Streams testing.
mkdir -p kafka-streams-testing
cd kafka-streams-testing
mvn archetype:generate -DgroupId=com.example.streams \
-DartifactId=kafka-streams-test \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
Configure Maven dependencies
Add Kafka Streams, TopologyTestDriver, and testing dependencies to your project.
4.0.0
com.example.streams
kafka-streams-test
1.0.0
jar
17
17
2.8.2
5.8.2
org.apache.kafka
kafka-streams
${kafka.version}
org.apache.kafka
kafka-streams-test-utils
${kafka.version}
test
org.junit.jupiter
junit-jupiter
${junit.version}
test
org.slf4j
slf4j-simple
1.7.36
org.apache.maven.plugins
maven-surefire-plugin
3.0.0-M7
Create Gradle build configuration
Alternative Gradle setup for projects preferring Gradle over Maven.
plugins {
id 'java'
id 'application'
}
group = 'com.example.streams'
version = '1.0.0'
java {
sourceCompatibility = '17'
targetCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.kafka:kafka-streams:2.8.2'
implementation 'org.slf4j:slf4j-simple:1.7.36'
testImplementation 'org.apache.kafka:kafka-streams-test-utils:2.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
exceptionFormat "full"
}
}
application {
mainClass = 'com.example.streams.StreamProcessor'
}
Create stream processing application
Build a sample Kafka Streams application for testing word count processing.
package com.example.streams;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.Produced;
import java.util.Arrays;
import java.util.Properties;
public class StreamProcessor {
public static Topology createTopology() {
StreamsBuilder builder = new StreamsBuilder();
KStream textLines = builder.stream("text-input");
KTable wordCounts = textLines
.flatMapValues(textLine -> Arrays.asList(textLine.toLowerCase().split("\\W+")))
.filter((key, word) -> word.length() > 0)
.groupBy((key, word) -> word)
.count();
wordCounts.toStream().to("word-count-output", Produced.with(Serdes.String(), Serdes.Long()));
return builder.build();
}
public static Properties getStreamsConfig() {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "word-count-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
return props;
}
public static void main(String[] args) {
Properties props = getStreamsConfig();
Topology topology = createTopology();
KafkaStreams streams = new KafkaStreams(topology, props);
streams.start();
Runtime.getRuntime().addShutdownHook(new Thread(streams::close));
}
}
Implement TopologyTestDriver tests
Create comprehensive unit tests using TopologyTestDriver for stream processing validation.
package com.example.streams;
import org.apache.kafka.common.serialization.LongDeserializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.kafka.streams.TestInputTopic;
import org.apache.kafka.streams.TestOutputTopic;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.TopologyTestDriver;
import org.apache.kafka.streams.test.TestRecord;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
class StreamProcessorTest {
private TopologyTestDriver testDriver;
private TestInputTopic inputTopic;
private TestOutputTopic outputTopic;
@BeforeEach
void setUp() {
Properties props = StreamProcessor.getStreamsConfig();
Topology topology = StreamProcessor.createTopology();
testDriver = new TopologyTestDriver(topology, props);
inputTopic = testDriver.createInputTopic(
"text-input",
new StringSerializer(),
new StringSerializer()
);
outputTopic = testDriver.createOutputTopic(
"word-count-output",
new StringDeserializer(),
new LongDeserializer()
);
}
@AfterEach
void tearDown() {
testDriver.close();
}
@Test
void shouldCountWordsCorrectly() {
// Given
inputTopic.pipeInput("key1", "hello world");
inputTopic.pipeInput("key2", "world test hello");
// When & Then
TestRecord record1 = outputTopic.readRecord();
assertEquals("hello", record1.key());
assertEquals(Long.valueOf(1), record1.value());
TestRecord record2 = outputTopic.readRecord();
assertEquals("world", record2.key());
assertEquals(Long.valueOf(1), record2.value());
TestRecord record3 = outputTopic.readRecord();
assertEquals("world", record3.key());
assertEquals(Long.valueOf(2), record3.value());
TestRecord record4 = outputTopic.readRecord();
assertEquals("test", record4.key());
assertEquals(Long.valueOf(1), record4.value());
TestRecord record5 = outputTopic.readRecord();
assertEquals("hello", record5.key());
assertEquals(Long.valueOf(2), record5.value());
assertTrue(outputTopic.isEmpty());
}
@Test
void shouldIgnoreEmptyStrings() {
// Given
inputTopic.pipeInput("key1", "");
inputTopic.pipeInput("key2", " ");
inputTopic.pipeInput("key3", "valid");
// When & Then
TestRecord record = outputTopic.readRecord();
assertEquals("valid", record.key());
assertEquals(Long.valueOf(1), record.value());
assertTrue(outputTopic.isEmpty());
}
@Test
void shouldHandleSpecialCharacters() {
// Given
inputTopic.pipeInput("key1", "hello,world!test@example");
// When & Then
outputTopic.readKeyValuesToMap();
assertFalse(outputTopic.isEmpty());
// Verify word separation by special characters
TestRecord record1 = outputTopic.readRecord();
TestRecord record2 = outputTopic.readRecord();
TestRecord record3 = outputTopic.readRecord();
TestRecord record4 = outputTopic.readRecord();
// All words should be counted as 1
assertEquals(Long.valueOf(1), record1.value());
assertEquals(Long.valueOf(1), record2.value());
assertEquals(Long.valueOf(1), record3.value());
assertEquals(Long.valueOf(1), record4.value());
}
}
Create advanced testing scenarios
Implement tests for error handling and edge cases in stream processing.
package com.example.streams;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.kafka.streams.TestInputTopic;
import org.apache.kafka.streams.TestOutputTopic;
import org.apache.kafka.streams.TopologyTestDriver;
import org.apache.kafka.streams.test.TestRecord;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.*;
class AdvancedStreamTest {
private TopologyTestDriver testDriver;
private TestInputTopic inputTopic;
@BeforeEach
void setUp() {
testDriver = new TopologyTestDriver(
StreamProcessor.createTopology(),
StreamProcessor.getStreamsConfig()
);
inputTopic = testDriver.createInputTopic(
"text-input",
new StringSerializer(),
new StringSerializer()
);
}
@AfterEach
void tearDown() {
testDriver.close();
}
@Test
void shouldHandleTimestampedRecords() {
// Given
Instant timestamp = Instant.parse("2023-01-01T12:00:00Z");
// When
inputTopic.pipeInput("key1", "timestamped message", timestamp);
// Then
TestOutputTopic outputTopic = testDriver.createOutputTopic(
"word-count-output",
new StringDeserializer(),
new org.apache.kafka.common.serialization.LongDeserializer()
);
TestRecord record = outputTopic.readRecord();
assertEquals(timestamp, record.getRecordTime());
}
@Test
void shouldProcessBatchRecords() {
// Given
java.util.List> inputRecords = java.util.Arrays.asList(
new TestRecord<>("key1", "batch one"),
new TestRecord<>("key2", "batch two"),
new TestRecord<>("key3", "batch three")
);
// When
inputTopic.pipeRecordList(inputRecords);
// Then
TestOutputTopic outputTopic = testDriver.createOutputTopic(
"word-count-output",
new StringDeserializer(),
new org.apache.kafka.common.serialization.LongDeserializer()
);
assertEquals(6, outputTopic.getQueueSize());
}
@Test
void shouldAdvanceTimeCorrectly() {
// Given
Duration timeAdvancement = Duration.ofMinutes(1);
// When
inputTopic.pipeInput("key1", "time test");
testDriver.advanceWallClockTime(timeAdvancement);
inputTopic.pipeInput("key2", "time test again");
// Then
assertTrue(testDriver.getAllStateStores().isEmpty() ||
!testDriver.getAllStateStores().isEmpty());
}
}
Configure test automation with Maven
Set up automated test execution and reporting with Maven surefire plugin.
cd kafka-streams-test
mvn clean compile
mvn test
# Run specific test class
mvn test -Dtest=StreamProcessorTest
Run tests with detailed output
mvn test -Dtest=StreamProcessorTest -DforkCount=1 -DreuseForks=false
Configure test automation with Gradle
Alternative Gradle configuration for automated testing and continuous integration.
# Build and run all tests
./gradlew test
Run specific test class
./gradlew test --tests StreamProcessorTest
Generate test reports
./gradlew test jacocoTestReport
// Add to existing build.gradle
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.7"
}
jacocoTestReport {
reports {
xml.required = false
csv.required = false
html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
}
}
Set up continuous integration testing
Create GitHub Actions workflow for automated testing on code changes.
name: Kafka Streams Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [11, 17]
steps:
- uses: actions/checkout@v3
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Run tests
run: mvn clean test
- name: Generate test report
run: mvn surefire-report:report
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results-java-${{ matrix.java-version }}
path: target/surefire-reports/
Verify your setup
# Check Java installation
java -version
javac -version
Verify Maven build
mvn --version
mvn clean compile
Run all tests
mvn test
Check test results
ls -la target/surefire-reports/
cat target/surefire-reports/TEST-*.xml
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Tests fail with ClassNotFoundException | Missing kafka-streams-test-utils dependency | Add test-utils dependency to pom.xml with test scope |
| TopologyTestDriver doesn't start | Invalid Streams configuration | Ensure APPLICATION_ID_CONFIG is set in test properties |
| Input/Output topics not found | Topic names don't match topology | Verify topic names in createInputTopic match stream builder |
| Serialization errors in tests | Mismatched serializers/deserializers | Use consistent Serde types for test topics and topology |
| Maven build fails | Java version incompatibility | Ensure Java 11+ and matching maven.compiler properties |
Next steps
- Set up Kafka Streams for real-time processing in production environments
- Implement advanced Kafka Streams applications with complex topologies
- Configure Schema Registry with Avro for structured data processing
- Set up backup verification and recovery testing for your data pipelines
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
KAFKA_VERSION="2.8.2"
SCALA_VERSION="2.13"
JAVA_VERSION="17"
INSTALL_DIR="/opt"
PROJECT_DIR="${1:-kafka-streams-testing}"
# Usage message
usage() {
echo "Usage: $0 [project-directory]"
echo " project-directory: Directory to create project in (default: kafka-streams-testing)"
exit 1
}
# Error handling
cleanup() {
echo -e "${RED}Installation failed. Cleaning up...${NC}"
sudo rm -rf "$INSTALL_DIR/kafka" 2>/dev/null || true
sudo rm -rf "$PROJECT_DIR" 2>/dev/null || true
}
trap cleanup ERR
# Logging functions
log_info() { echo -e "${GREEN}$1${NC}"; }
log_warn() { echo -e "${YELLOW}$1${NC}"; }
log_error() { echo -e "${RED}$1${NC}"; }
# Check arguments
if [[ $# -gt 1 ]]; then
usage
fi
# Check prerequisites
if [[ $EUID -eq 0 ]]; then
log_error "Please run this script as a regular user with sudo privileges"
exit 1
fi
if ! command -v sudo &> /dev/null; then
log_error "sudo is required but not installed"
exit 1
fi
# Auto-detect distribution
log_info "[1/8] Detecting operating system..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
JAVA_HOME_PATH="/usr/lib/jvm/java-${JAVA_VERSION}-openjdk-amd64"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
JAVA_HOME_PATH="/usr/lib/jvm/java-${JAVA_VERSION}-openjdk"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
JAVA_HOME_PATH="/usr/lib/jvm/java-${JAVA_VERSION}-openjdk"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect operating system"
exit 1
fi
log_info "Detected: $PRETTY_NAME"
# Update package manager
log_info "[2/8] Updating package manager..."
sudo $PKG_UPDATE
# Install Java, Maven, and Gradle
log_info "[3/8] Installing Java Development Kit and build tools..."
if [[ "$PKG_MGR" == "apt" ]]; then
sudo $PKG_INSTALL openjdk-${JAVA_VERSION}-jdk maven gradle wget tar
else
sudo $PKG_INSTALL java-${JAVA_VERSION}-openjdk-devel maven gradle wget tar
fi
# Verify Java installation
if ! java -version &> /dev/null; then
log_error "Java installation failed"
exit 1
fi
# Download and install Kafka
log_info "[4/8] Downloading and installing Apache Kafka..."
cd /tmp
wget -q "https://archive.apache.org/dist/kafka/${KAFKA_VERSION}/kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz"
sudo mkdir -p "$INSTALL_DIR"
sudo tar -xzf "kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz" -C "$INSTALL_DIR"
sudo mv "$INSTALL_DIR/kafka_${SCALA_VERSION}-${KAFKA_VERSION}" "$INSTALL_DIR/kafka"
# Set proper ownership and permissions
sudo chown -R root:root "$INSTALL_DIR/kafka"
sudo chmod -R 755 "$INSTALL_DIR/kafka"
# Clean up download
rm -f "kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz"
# Set up environment variables
log_info "[5/8] Configuring environment variables..."
cat << EOF | sudo tee /etc/profile.d/kafka-streams.sh > /dev/null
export JAVA_HOME=$JAVA_HOME_PATH
export KAFKA_HOME=$INSTALL_DIR/kafka
export PATH=\$PATH:\$KAFKA_HOME/bin:\$JAVA_HOME/bin
EOF
sudo chmod 644 /etc/profile.d/kafka-streams.sh
# Source environment variables
source /etc/profile.d/kafka-streams.sh
# Create Maven project structure
log_info "[6/8] Creating Maven project structure..."
if [[ -d "$PROJECT_DIR" ]]; then
log_warn "Project directory $PROJECT_DIR already exists. Removing it."
rm -rf "$PROJECT_DIR"
fi
mvn archetype:generate -DgroupId=com.example.streams \
-DartifactId=kafka-streams-test \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false -B
mv kafka-streams-test "$PROJECT_DIR"
cd "$PROJECT_DIR"
# Configure Maven pom.xml
log_info "[7/8] Configuring Maven dependencies..."
cat << 'EOF' > pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.streams</groupId>
<artifactId>kafka-streams-test</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<kafka.version>2.8.2</kafka.version>
<junit.version>5.8.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams-test-utils</artifactId>
<version>${kafka.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
</plugin>
</plugins>
</build>
</project>
EOF
# Create sample StreamProcessor class
mkdir -p src/main/java/com/example/streams
cat << 'EOF' > src/main/java/com/example/streams/StreamProcessor.java
package com.example.streams;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.Produced;
import java.util.Arrays;
public class StreamProcessor {
public static Topology createTopology() {
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> textLines = builder.stream("input-topic");
KTable<String, Long> wordCounts = textLines
.flatMapValues(textLine -> Arrays.asList(textLine.toLowerCase().split("\\W+")))
.groupBy((key, word) -> word)
.count();
wordCounts.toStream().to("output-topic", Produced.with(Serdes.String(), Serdes.Long()));
return builder.build();
}
}
EOF
# Create sample test class
mkdir -p src/test/java/com/example/streams
cat << 'EOF' > src/test/java/com/example/streams/StreamProcessorTest.java
package com.example.streams;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.kafka.streams.TestInputTopic;
import org.apache.kafka.streams.TestOutputTopic;
import org.apache.kafka.streams.TopologyTestDriver;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
class StreamProcessorTest {
private TopologyTestDriver testDriver;
private TestInputTopic<String, String> inputTopic;
private TestOutputTopic<String, Long> outputTopic;
@BeforeEach
void setUp() {
Properties props = new Properties();
props.put("application.id", "test-app");
props.put("bootstrap.servers", "dummy:1234");
testDriver = new TopologyTestDriver(StreamProcessor.createTopology(), props);
inputTopic = testDriver.createInputTopic("input-topic", new StringSerializer(), new StringSerializer());
outputTopic = testDriver.createOutputTopic("output-topic", new StringDeserializer(), org.apache.kafka.common.serialization.Serdes.Long().deserializer());
}
@AfterEach
void tearDown() {
testDriver.close();
}
@Test
void shouldCountWords() {
inputTopic.pipeInput("hello world");
inputTopic.pipeInput("hello kafka");
assertEquals(2, outputTopic.readValue()); // hello
assertEquals(1, outputTopic.readValue()); // world
assertEquals(1, outputTopic.readValue()); // kafka
assertTrue(outputTopic.isEmpty());
}
}
EOF
# Set proper permissions
chmod 755 src/main/java/com/example/streams
chmod 755 src/test/java/com/example/streams
chmod 644 src/main/java/com/example/streams/StreamProcessor.java
chmod 644 src/test/java/com/example/streams/StreamProcessorTest.java
chmod 644 pom.xml
# Verification
log_info "[8/8] Verifying installation..."
cd "$HOME"
# Test Java
if ! java -version &> /dev/null; then
log_error "Java verification failed"
exit 1
fi
# Test Maven
if ! mvn -version &> /dev/null; then
log_error "Maven verification failed"
exit 1
fi
# Test Kafka installation
if [[ ! -d "$INSTALL_DIR/kafka" ]]; then
log_error "Kafka installation verification failed"
exit 1
fi
# Test project compilation
cd "$PROJECT_DIR"
if ! mvn compile test-compile &> /dev/null; then
log_error "Project compilation verification failed"
exit 1
fi
log_info "✅ Kafka Streams testing framework installation completed successfully!"
log_info "📁 Project created in: $(pwd)"
log_info "🔧 To run tests: cd $PROJECT_DIR && mvn test"
log_info "⚠️ Please run 'source /etc/profile.d/kafka-streams.sh' or restart your shell to load environment variables"
Review the script before running. Execute with: bash install.sh