hackathon/docs/testing/test-package-structure.md
Christoph Wagner a7516834ad feat: Complete HSP architecture design with full requirement traceability
Add comprehensive architecture documentation for HTTP Sender Plugin (HSP):

  Architecture Design:
  - Hexagonal (ports & adapters) architecture validated as highly suitable
  - 7 port interfaces (3 primary, 4 secondary) with clean boundaries
  - 32 production classes mapped to 57 requirements
  - Virtual threads for 1000 concurrent HTTP endpoints
  - Producer-Consumer pattern with circular buffer
  - gRPC bidirectional streaming with 4MB batching

  Documentation Deliverables (20 files, ~150 pages):
  - Requirements catalog: All 57 requirements analyzed
  - Architecture docs: System design, component mapping, Java packages
  - Diagrams: 6 Mermaid diagrams (C4 model, sequence, data flow)
  - Traceability: Complete Req→Arch→Code→Test matrix (100% coverage)
  - Test strategy: 35+ test classes, 98% requirement coverage
  - Validation: Architecture approved, 0 critical gaps, LOW risk

  Key Metrics:
  - Requirements coverage: 100% (57/57)
  - Architecture mapping: 100%
  - Test coverage (planned): 94.6%
  - Critical gaps: 0
  - Overall risk: LOW

  Critical Issues Identified:
  - Buffer size conflict: Req-FR-25 (300) vs config spec (300,000)
  - Duplicate requirement IDs: Req-FR-25, Req-NFR-7/8, Req-US-1

  Technology Stack:
  - Java 25 (OpenJDK 25), Maven 3.9+, fat JAR packaging
  - gRPC Java 1.60+, Protocol Buffers 3.25+
  - JUnit 5, Mockito, WireMock for testing
  - Compliance: ISO-9001, EN 50716

  Status: Ready for implementation approval
2025-11-19 08:58:42 +01:00

24 KiB

Test Package Structure

Overview

This document defines the complete test package organization, test class structure, and mock server setup for the Log Data Collector system.

Test Source Directory Structure

src/test/
├── java/
│   └── com/
│       └── logcollector/
│           ├── unit/                      # Unit Tests (75% of suite)
│           │   ├── config/
│           │   │   ├── ConfigurationLoaderTest.java
│           │   │   ├── YamlParserTest.java
│           │   │   └── ValidationServiceTest.java
│           │   ├── serialization/
│           │   │   ├── DataSerializerTest.java
│           │   │   ├── JsonSerializerTest.java
│           │   │   └── ProtobufSerializerTest.java
│           │   ├── buffer/
│           │   │   ├── CircularBufferTest.java
│           │   │   ├── BufferOverflowHandlerTest.java
│           │   │   └── BufferThreadSafetyTest.java
│           │   ├── retry/
│           │   │   ├── RetryMechanismTest.java
│           │   │   ├── ExponentialBackoffTest.java
│           │   │   └── RetryPolicyTest.java
│           │   ├── health/
│           │   │   ├── HealthCheckEndpointTest.java
│           │   │   ├── HttpHealthCheckTest.java
│           │   │   └── GrpcHealthCheckTest.java
│           │   ├── collector/
│           │   │   ├── HttpCollectorTest.java
│           │   │   ├── EndpointSchedulerTest.java
│           │   │   └── ResponseParserTest.java
│           │   ├── transmitter/
│           │   │   ├── GrpcTransmitterTest.java
│           │   │   ├── ConnectionManagerTest.java
│           │   │   └── TransmissionQueueTest.java
│           │   └── startup/
│           │       ├── StartupSequenceTest.java
│           │       ├── ComponentInitializerTest.java
│           │       └── DependencyResolverTest.java
│           │
│           ├── integration/               # Integration Tests (20% of suite)
│           │   ├── collector/
│           │   │   ├── HttpCollectionIntegrationTest.java
│           │   │   └── MultiEndpointIntegrationTest.java
│           │   ├── transmitter/
│           │   │   ├── GrpcTransmissionIntegrationTest.java
│           │   │   └── ReconnectionIntegrationTest.java
│           │   ├── e2e/
│           │   │   ├── EndToEndDataFlowTest.java
│           │   │   └── BackpressureIntegrationTest.java
│           │   ├── config/
│           │   │   ├── ConfigurationFileIntegrationTest.java
│           │   │   └── ConfigurationReloadIntegrationTest.java
│           │   └── buffer/
│           │       ├── CircularBufferIntegrationTest.java
│           │       └── BufferPerformanceIntegrationTest.java
│           │
│           ├── performance/               # Performance Tests
│           │   ├── PerformanceConcurrentEndpointsTest.java
│           │   ├── PerformanceMemoryUsageTest.java
│           │   ├── PerformanceVirtualThreadTest.java
│           │   └── PerformanceStartupTimeTest.java
│           │
│           ├── reliability/               # Reliability Tests
│           │   ├── ReliabilityStartupSequenceTest.java
│           │   ├── ReliabilityGrpcRetryTest.java
│           │   ├── ReliabilityHttpFailureTest.java
│           │   ├── ReliabilityBufferOverflowTest.java
│           │   └── ReliabilityPartialFailureTest.java
│           │
│           ├── compliance/                # Compliance Tests
│           │   ├── ComplianceErrorDetectionTest.java
│           │   ├── ComplianceIso9001Test.java
│           │   ├── ComplianceEn50716Test.java
│           │   └── ComplianceAuditLoggingTest.java
│           │
│           └── util/                      # Test Utilities
│               ├── mock/
│               │   ├── HttpMockServerSetup.java
│               │   ├── GrpcMockServerSetup.java
│               │   └── MockClockProvider.java
│               ├── builder/
│               │   ├── TestDataFactory.java
│               │   ├── TestConfigurationBuilder.java
│               │   ├── TestEndpointBuilder.java
│               │   └── TestLogEntryBuilder.java
│               ├── assertion/
│               │   ├── CustomAssertions.java
│               │   └── PerformanceAssertions.java
│               └── extension/
│                   ├── MockServerExtension.java
│                   ├── GrpcServerExtension.java
│                   └── PerformanceTestExtension.java
│
└── resources/
    ├── config/
    │   ├── test-config.yaml                  # Valid test configuration
    │   ├── test-config-invalid.yaml          # Invalid format
    │   ├── test-config-minimal.yaml          # Minimal valid config
    │   └── test-config-maximal.yaml          # Maximum complexity config
    ├── data/
    │   ├── sample-log-entries.json           # Sample log data
    │   └── large-payload.json                # Large payload test data
    ├── wiremock/
    │   ├── mappings/                         # WireMock stub mappings
    │   │   ├── health-check-success.json
    │   │   ├── health-check-failure.json
    │   │   ├── log-endpoint-success.json
    │   │   └── log-endpoint-timeout.json
    │   └── __files/                          # WireMock response files
    │       ├── sample-response.json
    │       └── error-response.json
    ├── proto/
    │   └── test-log-data.proto               # Test Protocol Buffer definitions
    ├── logback-test.xml                      # Test logging configuration
    └── junit-platform.properties             # JUnit configuration

Test Class Templates

Unit Test Template

package com.logcollector.unit.config;

import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
 * Unit tests for ConfigurationLoader component.
 *
 * @validates Req-FR-11 - Configuration file detection
 * @validates Req-FR-12 - Configuration parsing
 * @validates Req-FR-13 - Configuration validation
 * @validates Req-Norm-3 - Error detection
 */
@DisplayName("Configuration Loader Unit Tests")
@Tag("unit")
@Tag("config")
class ConfigurationLoaderTest {

    @Mock
    private FileSystem fileSystem;

    @Mock
    private YamlParser yamlParser;

    @InjectMocks
    private ConfigurationLoader configurationLoader;

    private AutoCloseable mocks;

    @BeforeEach
    void setUp() {
        mocks = MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void tearDown() throws Exception {
        mocks.close();
    }

    @Nested
    @DisplayName("Configuration File Detection")
    class FileDetectionTests {

        @Test
        @DisplayName("should detect config file when file exists")
        void shouldDetectConfigFile_whenFileExists() {
            // Arrange
            when(fileSystem.exists("/etc/logcollector/config.yaml"))
                .thenReturn(true);

            // Act
            boolean result = configurationLoader.detectConfigFile();

            // Assert
            assertThat(result).isTrue();
            verify(fileSystem).exists("/etc/logcollector/config.yaml");
        }

        @Test
        @DisplayName("should throw exception when file not found")
        void shouldThrowException_whenFileNotFound() {
            // Arrange
            when(fileSystem.exists(anyString())).thenReturn(false);

            // Act & Assert
            assertThatThrownBy(() -> configurationLoader.load())
                .isInstanceOf(ConfigurationNotFoundException.class)
                .hasMessageContaining("Configuration file not found");
        }
    }

    @Nested
    @DisplayName("Configuration Parsing")
    class ParsingTests {
        // Parsing test methods...
    }

    @Nested
    @DisplayName("Configuration Validation")
    class ValidationTests {
        // Validation test methods...
    }
}

Integration Test Template

package com.logcollector.integration.collector;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.logcollector.util.extension.MockServerExtension;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.assertj.core.api.Assertions.*;

/**
 * Integration tests for HTTP collection with mock server.
 *
 * @validates Req-NFR-7 - HTTP health check endpoint
 * @validates Req-FR-14 - HTTP endpoint collection
 * @validates Req-FR-15 - Response parsing
 */
@DisplayName("HTTP Collection Integration Tests")
@Tag("integration")
@Tag("http")
@ExtendWith(MockServerExtension.class)
class HttpCollectionIntegrationTest {

    private WireMockServer wireMockServer;
    private HttpCollector httpCollector;

    @BeforeEach
    void setUp(WireMockServer server) {
        this.wireMockServer = server;
        this.httpCollector = new HttpCollector(
            "http://localhost:" + server.port()
        );
    }

    @Test
    @DisplayName("should collect from mock endpoint when server running")
    void shouldCollectFromMockEndpoint_whenServerRunning() {
        // Arrange
        wireMockServer.stubFor(get(urlEqualTo("/logs"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBodyFile("sample-response.json")));

        // Act
        LogData result = httpCollector.collect("/logs");

        // Assert
        assertThat(result).isNotNull();
        assertThat(result.getEntries()).isNotEmpty();

        // Verify interaction
        wireMockServer.verify(1, getRequestedFor(urlEqualTo("/logs")));
    }

    @Test
    @DisplayName("should handle multiple endpoints concurrently")
    void shouldHandleMultipleEndpoints_concurrently() {
        // Test implementation...
    }
}

Performance Test Template

package com.logcollector.performance;

import org.junit.jupiter.api.*;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.*;

/**
 * Performance tests for concurrent endpoint handling.
 *
 * @validates Req-NFR-1 - 1000 concurrent endpoints support
 */
@DisplayName("Performance: Concurrent Endpoints")
@Tag("performance")
class PerformanceConcurrentEndpointsTest {

    @Test
    @DisplayName("should handle 1000 endpoints concurrently")
    @Timeout(value = 60, unit = TimeUnit.SECONDS)
    void shouldHandle1000Endpoints_concurrently() throws Exception {
        // Arrange
        List<String> endpoints = IntStream.range(0, 1000)
            .mapToObj(i -> "http://endpoint-" + i + ".test/logs")
            .collect(Collectors.toList());

        HttpCollector collector = new HttpCollector();

        // Act
        long startTime = System.nanoTime();
        List<CompletableFuture<LogData>> futures = endpoints.stream()
            .map(collector::collectAsync)
            .toList();

        List<LogData> results = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        ).thenApply(v -> futures.stream()
            .map(CompletableFuture::join)
            .toList()
        ).get();

        long duration = System.nanoTime() - startTime;

        // Assert
        assertThat(results).hasSize(1000);
        assertThat(duration).isLessThan(TimeUnit.MINUTES.toNanos(1));

        // Report metrics
        System.out.printf("Collected 1000 endpoints in %.2f seconds%n",
            duration / 1_000_000_000.0);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void benchmarkEndpointCollection() {
        // JMH benchmark implementation...
    }
}

Mock Server Setup

WireMock HTTP Server

package com.logcollector.util.mock;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import static com.github.tomakehurst.wiremock.client.WireMock.*;

/**
 * Mock HTTP server setup for testing HTTP collection.
 *
 * @validates Req-NFR-7 - Mock HTTP server requirement
 */
public class HttpMockServerSetup {

    private final WireMockServer server;

    public HttpMockServerSetup() {
        this.server = new WireMockServer(
            WireMockConfiguration.options()
                .dynamicPort()
                .usingFilesUnderClasspath("wiremock")
        );
    }

    public void start() {
        server.start();
        configureDefaultStubs();
    }

    public void stop() {
        server.stop();
    }

    public int getPort() {
        return server.port();
    }

    public WireMockServer getServer() {
        return server;
    }

    private void configureDefaultStubs() {
        // Health check success
        server.stubFor(get(urlEqualTo("/health"))
            .willReturn(aResponse()
                .withStatus(200)
                .withBody("{\"status\":\"UP\"}")));

        // Log endpoint success
        server.stubFor(get(urlPathMatching("/logs.*"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBodyFile("sample-response.json")));

        // Simulate timeout
        server.stubFor(get(urlEqualTo("/slow"))
            .willReturn(aResponse()
                .withStatus(200)
                .withFixedDelay(10000)));

        // Simulate error
        server.stubFor(get(urlEqualTo("/error"))
            .willReturn(aResponse()
                .withStatus(500)
                .withBody("{\"error\":\"Internal Server Error\"}")));
    }
}

gRPC Mock Server

package com.logcollector.util.mock;

import io.grpc.*;
import io.grpc.inprocess.*;
import io.grpc.stub.StreamObserver;
import java.io.IOException;

/**
 * Mock gRPC server setup for testing transmission.
 *
 * @validates Req-NFR-8 - Mock gRPC server requirement
 */
public class GrpcMockServerSetup {

    private final String serverName;
    private final Server server;
    private final MockLogDataService mockService;

    public GrpcMockServerSetup() {
        this.serverName = InProcessServerBuilder.generateName();
        this.mockService = new MockLogDataService();
        this.server = InProcessServerBuilder
            .forName(serverName)
            .directExecutor()
            .addService(mockService)
            .build();
    }

    public void start() throws IOException {
        server.start();
    }

    public void stop() {
        server.shutdownNow();
    }

    public String getServerName() {
        return serverName;
    }

    public MockLogDataService getMockService() {
        return mockService;
    }

    public ManagedChannel createChannel() {
        return InProcessChannelBuilder
            .forName(serverName)
            .directExecutor()
            .build();
    }

    /**
     * Mock implementation of LogDataService for testing.
     */
    public static class MockLogDataService
            extends LogDataServiceGrpc.LogDataServiceImplBase {

        private int callCount = 0;
        private boolean shouldFail = false;

        @Override
        public void sendLogData(
                LogDataRequest request,
                StreamObserver<LogDataResponse> responseObserver) {

            callCount++;

            if (shouldFail) {
                responseObserver.onError(
                    Status.UNAVAILABLE
                        .withDescription("Mock failure")
                        .asRuntimeException()
                );
                return;
            }

            LogDataResponse response = LogDataResponse.newBuilder()
                .setSuccess(true)
                .setMessageId(UUID.randomUUID().toString())
                .build();

            responseObserver.onNext(response);
            responseObserver.onCompleted();
        }

        public int getCallCount() {
            return callCount;
        }

        public void setShouldFail(boolean shouldFail) {
            this.shouldFail = shouldFail;
        }

        public void reset() {
            callCount = 0;
            shouldFail = false;
        }
    }
}

Test Data Builders

Test Configuration Builder

package com.logcollector.util.builder;

import com.logcollector.config.Configuration;
import com.logcollector.config.EndpointConfig;
import com.logcollector.config.GrpcConfig;
import java.util.*;

/**
 * Fluent builder for test Configuration objects.
 */
public class TestConfigurationBuilder {

    private List<EndpointConfig> endpoints = new ArrayList<>();
    private String grpcHost = "localhost";
    private int grpcPort = 9090;
    private int bufferSize = 10000;
    private int retryMaxAttempts = 3;
    private int retryBaseDelay = 100;

    public static TestConfigurationBuilder aConfiguration() {
        return new TestConfigurationBuilder();
    }

    public TestConfigurationBuilder withEndpoint(EndpointConfig endpoint) {
        this.endpoints.add(endpoint);
        return this;
    }

    public TestConfigurationBuilder withEndpoints(int count) {
        for (int i = 0; i < count; i++) {
            endpoints.add(TestEndpointBuilder.anEndpoint()
                .withUrl("http://endpoint-" + i + ".test/logs")
                .withSchedule("0/30 * * * * ?")
                .build());
        }
        return this;
    }

    public TestConfigurationBuilder withGrpcHost(String host) {
        this.grpcHost = host;
        return this;
    }

    public TestConfigurationBuilder withGrpcPort(int port) {
        this.grpcPort = port;
        return this;
    }

    public TestConfigurationBuilder withBufferSize(int size) {
        this.bufferSize = size;
        return this;
    }

    public TestConfigurationBuilder withRetryConfig(int maxAttempts, int baseDelay) {
        this.retryMaxAttempts = maxAttempts;
        this.retryBaseDelay = baseDelay;
        return this;
    }

    public Configuration build() {
        return Configuration.builder()
            .endpoints(endpoints)
            .grpcConfig(GrpcConfig.builder()
                .host(grpcHost)
                .port(grpcPort)
                .build())
            .bufferSize(bufferSize)
            .retryMaxAttempts(retryMaxAttempts)
            .retryBaseDelayMs(retryBaseDelay)
            .build();
    }
}

JUnit Extensions

Mock Server Extension

package com.logcollector.util.extension;

import com.logcollector.util.mock.HttpMockServerSetup;
import org.junit.jupiter.api.extension.*;

/**
 * JUnit 5 extension for WireMock server lifecycle management.
 */
public class MockServerExtension implements BeforeEachCallback, AfterEachCallback {

    private static final String SERVER_KEY = "mockServer";

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        HttpMockServerSetup server = new HttpMockServerSetup();
        server.start();

        context.getStore(ExtensionContext.Namespace.GLOBAL)
            .put(SERVER_KEY, server);
    }

    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        HttpMockServerSetup server = context.getStore(
            ExtensionContext.Namespace.GLOBAL
        ).get(SERVER_KEY, HttpMockServerSetup.class);

        if (server != null) {
            server.stop();
        }
    }
}

Test Resource Files

test-config.yaml

# Valid test configuration
endpoints:
  - url: "http://endpoint1.test/logs"
    schedule: "0/30 * * * * ?"
    timeout: 5000
  - url: "http://endpoint2.test/logs"
    schedule: "0/60 * * * * ?"
    timeout: 5000

grpc:
  host: "localhost"
  port: 9090
  tls: false

buffer:
  size: 10000
  overflow: "overwrite"

retry:
  maxAttempts: 3
  baseDelayMs: 100
  maxDelayMs: 5000

logback-test.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>

    <!-- Reduce noise from test frameworks -->
    <logger name="org.springframework" level="WARN"/>
    <logger name="org.hibernate" level="WARN"/>
    <logger name="io.grpc" level="WARN"/>
    <logger name="com.github.tomakehurst.wiremock" level="WARN"/>
</configuration>

Maven Test Configuration

pom.xml (Test Section)

<build>
    <testSourceDirectory>src/test/java</testSourceDirectory>
    <testResources>
        <testResource>
            <directory>src/test/resources</directory>
        </testResource>
    </testResources>

    <plugins>
        <!-- Surefire for unit tests -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
            <configuration>
                <includes>
                    <include>**/*Test.java</include>
                </includes>
                <excludes>
                    <exclude>**/integration/**</exclude>
                    <exclude>**/performance/**</exclude>
                </excludes>
                <parallel>classes</parallel>
                <threadCount>4</threadCount>
            </configuration>
        </plugin>

        <!-- Failsafe for integration tests -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>3.2.5</version>
            <configuration>
                <includes>
                    <include>**/integration/**/*Test.java</include>
                </includes>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

<profiles>
    <!-- Performance testing profile -->
    <profile>
        <id>performance-tests</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <includes>
                            <include>**/performance/**/*Test.java</include>
                        </includes>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Version: 1.0 Last Updated: 2025-11-19 Author: Test Strategist Agent Status: Complete - Test Infrastructure Defined