57 Unit Tests für Producer und Consumer

Unit Tests bilden das Fundament testbarer Event-Driven Architecture. Anders als bei traditionellen REST-basierten Systemen müssen wir hier asynchrone Kommunikation und Event-Infrastruktur testen, ohne tatsächliche Broker zu starten. Das Ziel: schnelle, isolierte Tests, die das Verhalten unserer Producer und Consumer verifizieren.

57.1 Das Testproblem in Event-Driven Architecture

Angenommen Ihr OrderService produziert OrderPlaced-Events und Ihr PaymentService konsumiert diese. Wie testen Sie diese Komponenten isoliert, ohne Kafka zu starten? Wie simulieren Sie Event-Empfang ohne echte Nachrichten?

Die Antwort liegt in strategischer Abstraktion und geschicktem Mocking der Event-Infrastruktur.

57.2 Mocking Event Infrastructure

57.2.1 Abstraktionsebenen für Producer

Ein testbarer Producer trennt Geschäftslogik von der Infrastruktur. Betrachten Sie diese Struktur:

Spring Boot - Testbarer OrderService:

@Service
public class OrderService {
    private final EventPublisher eventPublisher;
    
    public OrderService(EventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public void placeOrder(CreateOrderRequest request) {
        // Geschäftslogik
        Order order = new Order(request.getCustomerId(), request.getItems());
        order.calculateTotal();
        
        // Event-Erzeugung
        OrderPlacedEvent event = OrderPlacedEvent.builder()
            .orderId(order.getId())
            .customerId(order.getCustomerId())
            .totalAmount(order.getTotal())
            .build();
            
        eventPublisher.publish("order.placed.v1", event);
    }
}

Python - Testbarer OrderService:

class OrderService:
    def __init__(self, event_publisher: EventPublisher):
        self.event_publisher = event_publisher
    
    def place_order(self, request: CreateOrderRequest):
        # Geschäftslogik
        order = Order(request.customer_id, request.items)
        order.calculate_total()
        
        # Event-Erzeugung
        event = OrderPlacedEvent(
            order_id=order.id,
            customer_id=order.customer_id,
            total_amount=order.total
        )
        
        self.event_publisher.publish("order.placed.v1", event)

57.2.2 Mock-Strategien im Vergleich

Ansatz Vorteile Nachteile Einsatzbereich
Interface-Mock Vollständige Kontrolle Erfordert Abstraktion Unit Tests
Test-Implementation Realitätsnahe Simulation Mehr Setup-Code Integrationstests
In-Memory-Broker Echte Infrastruktur Langsamere Tests Component Tests

57.2.3 Unit Test für Producer

Spring Boot Test:

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    
    @Mock
    private EventPublisher eventPublisher;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    void shouldPublishOrderPlacedEventWhenOrderIsCreated() {
        // Given
        CreateOrderRequest request = CreateOrderRequest.builder()
            .customerId("customer-123")
            .items(List.of(new OrderItem("product-1", 2)))
            .build();
        
        // When
        orderService.placeOrder(request);
        
        // Then
        ArgumentCaptor<OrderPlacedEvent> eventCaptor = 
            ArgumentCaptor.forClass(OrderPlacedEvent.class);
            
        verify(eventPublisher).publish(
            eq("order.placed.v1"), 
            eventCaptor.capture()
        );
        
        OrderPlacedEvent publishedEvent = eventCaptor.getValue();
        assertThat(publishedEvent.getCustomerId()).isEqualTo("customer-123");
        assertThat(publishedEvent.getOrderId()).isNotNull();
    }
}

Python Test:

class TestOrderService:
    def test_should_publish_order_placed_event_when_order_created(self):
        # Given
        mock_publisher = Mock(spec=EventPublisher)
        order_service = OrderService(mock_publisher)
        
        request = CreateOrderRequest(
            customer_id="customer-123",
            items=[OrderItem("product-1", 2)]
        )
        
        # When
        order_service.place_order(request)
        
        # Then
        mock_publisher.publish.assert_called_once()
        topic, event = mock_publisher.publish.call_args[0]
        
        assert topic == "order.placed.v1"
        assert event.customer_id == "customer-123"
        assert event.order_id is not None

57.2.4 Abstraktionsebenen für Consumer

Consumer benötigen ähnliche Abstraktionen. Der PaymentService sollte Event-Empfang von Verarbeitungslogik trennen:

Spring Boot - Testbarer PaymentService:

@Service
public class PaymentService {
    
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // Geschäftslogik ohne Kafka-Details
        Payment payment = Payment.builder()
            .orderId(event.getOrderId())
            .amount(event.getTotalAmount())
            .status(PaymentStatus.PENDING)
            .build();
            
        processPayment(payment);
    }
    
    private void processPayment(Payment payment) {
        // Zahlungsverarbeitung
    }
}

Unit Test für Consumer:

@Test
void shouldCreatePendingPaymentWhenOrderPlacedEventReceived() {
    // Given
    OrderPlacedEvent event = OrderPlacedEvent.builder()
        .orderId("order-123")
        .customerId("customer-456")
        .totalAmount(BigDecimal.valueOf(99.99))
        .build();
    
    // When
    paymentService.handleOrderPlaced(event);
    
    // Then
    // Verifikation über Repository-Mock oder State-Assertions
}

57.3 Test Data Management

57.3.1 Test Data Builder Pattern

Konsistente Testdaten reduzieren Wartungsaufwand und verbessern Lesbarkeit:

Java Test Data Builder:

public class OrderPlacedEventTestDataBuilder {
    private String orderId = "default-order-id";
    private String customerId = "default-customer-id";
    private BigDecimal totalAmount = BigDecimal.valueOf(100.00);
    
    public static OrderPlacedEventTestDataBuilder anOrderPlacedEvent() {
        return new OrderPlacedEventTestDataBuilder();
    }
    
    public OrderPlacedEventTestDataBuilder withOrderId(String orderId) {
        this.orderId = orderId;
        return this;
    }
    
    public OrderPlacedEventTestDataBuilder withAmount(BigDecimal amount) {
        this.totalAmount = amount;
        return this;
    }
    
    public OrderPlacedEvent build() {
        return OrderPlacedEvent.builder()
            .orderId(orderId)
            .customerId(customerId)
            .totalAmount(totalAmount)
            .timestamp(Instant.now())
            .build();
    }
}

Verwendung im Test:

@Test
void shouldRejectHighValueOrders() {
    // Given
    OrderPlacedEvent highValueEvent = anOrderPlacedEvent()
        .withAmount(BigDecimal.valueOf(10000.00))
        .build();
    
    // When & Then
    assertThatThrownBy(() -> paymentService.handleOrderPlaced(highValueEvent))
        .isInstanceOf(PaymentLimitExceededException.class);
}

57.3.2 Test-spezifische Event-Varianten

Verschiedene Testszenarien erfordern verschiedene Event-Ausprägungen:

Testszenario Event-Charakteristik Builder-Methode
Happy Path Gültige Standardwerte aValidOrderPlacedEvent()
Edge Case Grenzwerte anOrderWithMaxAmount()
Error Case Ungültige Daten anInvalidOrderEvent()
Performance Große Datenmengen aBulkOrderEvent()

57.4 Isolation Techniques

57.4.1 Dependency Isolation

Echte Unit Tests isolieren die zu testende Einheit von allen Abhängigkeiten:

Spring Boot - Vollständige Isolation:

@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
    
    @Mock private PaymentRepository paymentRepository;
    @Mock private PaymentGateway paymentGateway;
    @Mock private EventPublisher eventPublisher;
    
    @InjectMocks private PaymentService paymentService;
    
    @Test
    void shouldPublishPaymentProcessedEventAfterSuccessfulPayment() {
        // Given
        OrderPlacedEvent orderEvent = anOrderPlacedEvent().build();
        
        when(paymentGateway.processPayment(any()))
            .thenReturn(PaymentResult.success("transaction-123"));
        
        // When
        paymentService.handleOrderPlaced(orderEvent);
        
        // Then
        verify(eventPublisher).publish(
            eq("payment.processed.v1"), 
            any(PaymentProcessedEvent.class)
        );
    }
}

57.4.2 State Verification vs. Behavior Verification

Unit Tests können auf zwei Arten verifizieren:

State Verification - “Was ist das Ergebnis?”

@Test
void shouldCreatePaymentRecordWithCorrectAmount() {
    // When
    paymentService.handleOrderPlaced(orderEvent);
    
    // Then - State Verification
    Payment createdPayment = paymentService.findPaymentForOrder("order-123");
    assertThat(createdPayment.getAmount()).isEqualTo(BigDecimal.valueOf(99.99));
}

Behavior Verification - “Welche Interaktionen fanden statt?”

@Test
void shouldCallPaymentGatewayWithCorrectAmount() {
    // When
    paymentService.handleOrderPlaced(orderEvent);
    
    // Then - Behavior Verification
    ArgumentCaptor<PaymentRequest> captor = 
        ArgumentCaptor.forClass(PaymentRequest.class);
    verify(paymentGateway).processPayment(captor.capture());
    
    assertThat(captor.getValue().getAmount()).isEqualTo(BigDecimal.valueOf(99.99));
}

57.4.3 Thread-Isolation bei asynchroner Verarbeitung

Event-Consumer arbeiten oft asynchron. Tests müssen diese Parallelität handhaben:

Python - Async Consumer Test:

@pytest.mark.asyncio
async def test_async_event_processing():
    # Given
    mock_publisher = AsyncMock(spec=EventPublisher)
    service = AsyncPaymentService(mock_publisher)
    
    event = anOrderPlacedEvent().build()
    
    # When
    await service.handle_order_placed(event)
    
    # Then
    mock_publisher.publish.assert_called_once()

57.4.4 Test-Isolation-Checklist

Für jeden Unit Test sollten Sie prüfen:

Die Kunst des Unit Testing in Event-Driven Architecture liegt darin, die Asynchronität zu abstrahieren, ohne die Geschäftslogik zu verfälschen. Mit den richtigen Abstraktionen und Mock-Strategien entstehen schnelle, verlässliche Tests, die Refactoring unterstützen und Regression verhindern.